├── .gitignore ├── LICENSE ├── Modern-CMake-for-C++-2ed.tex ├── README.md ├── book ├── ccs.tex ├── content │ ├── Contributors.tex │ ├── Foreword.tex │ ├── Preface.tex │ ├── Reviewers.tex │ ├── chapter1 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ ├── 7.tex │ │ ├── 8.tex │ │ └── images │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ └── 4.png │ ├── chapter10 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ └── images │ │ │ └── 1.png │ ├── chapter11 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ ├── 7.tex │ │ ├── 8.tex │ │ ├── 9.tex │ │ └── images │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ └── 3.png │ ├── chapter12 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ └── images │ │ │ └── 1.png │ ├── chapter13 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ └── images │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ └── 4.png │ ├── chapter14 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ ├── 7.tex │ │ ├── 8.tex │ │ └── images │ │ │ └── 1.png │ ├── chapter15 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ ├── 7.tex │ │ ├── 8.tex │ │ ├── 9.tex │ │ └── images │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ ├── 4.png │ │ │ ├── 5.png │ │ │ ├── 6.png │ │ │ └── 7.png │ ├── chapter16 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ ├── 7.tex │ │ ├── 8.tex │ │ └── images │ │ │ └── 1.png │ ├── chapter17 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ └── 5.tex │ ├── chapter2 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ ├── 7.tex │ │ ├── 8.tex │ │ └── images │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ └── 3.png │ ├── chapter3 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ └── images │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ ├── 4.png │ │ │ ├── 5.png │ │ │ └── 6.png │ ├── chapter4 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ ├── 7.tex │ │ ├── 8.tex │ │ ├── 9.tex │ │ └── images │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ └── 4.png │ ├── chapter5 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ └── images │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ └── 3.png │ ├── chapter6 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ ├── 7.tex │ │ ├── 8.tex │ │ └── images │ │ │ └── 1.png │ ├── chapter7 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ └── 7.tex │ ├── chapter8 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ ├── 7.tex │ │ ├── 8.tex │ │ └── images │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ └── 4.png │ └── chapter9 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ └── images │ │ └── 1.png └── index.tex └── cover.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.pdf 2 | *.aux 3 | *.log 4 | *.out 5 | *.gz 6 | *.py 7 | *toc 8 | *.listing 9 | *.synctex(busy) 10 | /latex-test/ 11 | /test-pandoc/ 12 | *.epub 13 | *.pygtex 14 | *.pygstyle 15 | 16 | /_minted-Modern-CMake-for-C++-2ed/ 17 | -------------------------------------------------------------------------------- /Modern-CMake-for-C++-2ed.tex: -------------------------------------------------------------------------------- 1 | 2 | %\special{dvipdfmx:config z 0} %取消PDF压缩,加快速度,最终版本生成的时候最好把这句话注释掉 3 | 4 | \include{book/ccs.tex} 5 | 6 | \begin{document} 7 | \begin{sloppypar} %latex中一行文字出现溢出问题的解决方法 8 | %\maketitle 9 | 10 | \subfile{book/index.tex} 11 | 12 | \end{sloppypar} 13 | \end{document} 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Modern CMake for C++ 2 | *Second Edition* 3 | 4 | *轻松构建前沿C++代码,提供高质量的解决方案* 5 | 6 | * 作者:Rafał Świdziński 7 | * 译者:陈晓伟 8 | * Packt Publishing Ltd. (出版于: 2024年5月8日) 9 | 10 | > [!IMPORTANT] 11 | > 翻译是译者用自己的思想,换一种语言,对原作者想法的重新阐释。鉴于我的学识所限,误解和错译在所难免。如果你能买到本书的原版,且有能力阅读英文,请直接去读原文。因为与之相较,我的译文可能根本不值得一读。 12 | > 13 | >

— 云风,程序员修炼之道第2版译者

14 | 15 | ## 本书概述 16 | 17 | 创建顶级软件并非易事。在线研究这个主题的开发者难以确定哪些建议是当前的,哪些方法已经更新,或已经有更好的实践方式。此外,大多数资源以混乱的方式解释过程,缺乏适当的背景、上下文和结构。 18 | 19 | 《Modern CMake for C++》提供了一个端到端的指南,通过全面处理C++解决方案的构建,提供了更简单的体验。不仅介绍如何在项目中使用CMake,还强调了如何使项目保持可维护性、优雅和简洁。该指南会协助读者们自动化完成许多项目中的常见任务,包括构建、测试和打包。 20 | 21 | 本书还会介绍如何组织源目录、构建目标和创建包。随着了解的深入,将学习编译和链接可执行文件和库,详细理解这些过程,并优化每个步骤以获得最佳结果。此外,还会介绍如何将外部依赖项(如第三方库、测试框架、程序分析工具和文档生成器)整合到自己的项目中。最后,将学习如何导出、安装和打包解决方案,以供内部和外部使用。 22 | 23 | 阅读完这本书后,将能以专业水平使用CMake。 24 | 25 | ## 作者简介 26 | 27 | **Rafał Świdziński**是谷歌的一名资深工程师,拥有超过12年的全栈开发经验。他领导过思科Meraki、亚马逊和爱立信等行业巨头的项目,居住在伦敦。他始终站在技术进步的前沿,参与了许多创业项目,最近转向了医疗保健领域的AI。Rafał重视顶尖的代码质量和工艺,也会通过YouTube频道和出版的书籍分享见解。 28 | 29 | 致Zoe --- 无汝,无书(如果没有你,我无法写出这本书) 30 | 31 | 32 | 33 | ## 本书相关 34 | 35 | * github翻译地址:https://github.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed 36 | 37 | * 第一版译文地址:https://github.com/xiaoweiChen/Modern-CMake-for-Cpp 38 | 39 | * 译文的LaTeX 环境配置:https://www.cnblogs.com/1625--H/p/11524968.html 40 | 41 | * 禁用拼写检查:https://blog.csdn.net/weixin_39278265/article/details/87931348 42 | 43 | * 使用xelatex编译时需要添加`-shell-escape`和`-8bit`选项,例如: 44 | 45 | `xelatex -synctex=1 -interaction=nonstopmode -shell-escape -8bit "Modern-CMake-for-C++-2ed".tex` 46 | 47 | * 为了内容中表格和目录索引能正常生成,至少需要连续编译两次 48 | 49 | * Latex中的中文字体([思源黑体](https://github.com/adobe-fonts/source-han-sans))和英文字体([Hack](https://github.com/source-foundry/Hack-windows-installer/releases/tag/v1.6.0)),需要安装后自行配置。如何配置请参考主book/css.tex顶部关于字体的信息。 50 | 51 | * vscode中配置LaTeX:https://blog.csdn.net/Ruins_LEE/article/details/123555016 52 | 53 | -------------------------------------------------------------------------------- /book/content/Contributors.tex: -------------------------------------------------------------------------------- 1 | \textbf{Rafał Świdziński}是谷歌的一名资深工程师,拥有超过12年的全栈开发经验。他领导过思科Meraki、亚马逊和爱立信等行业巨头的项目,体现了对创新的承诺。作为一名自愿选择居住在伦敦的人,他始终站在技术进步的前沿,参与了许多个人创业项目。最近他转向医疗保健领域的AI。Rafał重视顶尖的代码质量和工艺,也会通过YouTube频道和出版的书籍分享见解。 2 | 3 | \textit{致Zoe --- 无汝,无书(如果没有你,我无法写出这本书)} 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /book/content/Foreword.tex: -------------------------------------------------------------------------------- 1 | 2 | 在C++的不断发展的中,掌握CMake对于致力于编写高效、可维护和可扩展程序的开发者来说是必不可少的。《Modern CMake for C++》(由Rafał Świdziński撰写)就像一座灯塔,引导开发者们穿梭于CMake之中。 3 | 4 | 这本书不仅仅是一本手册,也是一次旅行。从基础开始,随着章节的深入,读者将掌握高阶技术。本书的独特之处在于其实用的方法,现实例子和最佳实践贯穿全文,确保读者不仅理解概念,而且知道如何在项目中应用。 5 | 6 | 读完这本书后,读者不仅会对CMake有深入的理解,会对驾驭C++有更多的信心。这是有助于编写更干净、更高效代码所需的知识和技能,进而成为熟练的CMake开发者。 7 | 8 | 《Modern CMake for C++》不仅仅是一本书,也是一个工具,将提升读者的C++开发技能。无论是初学者还是专家,这本书都将有助你掌握CMake,使代码更加健壮、可维护和可扩展。 9 | 10 | \begin{center} 11 | \textit{Alexander Kushnir} 12 | 13 | \textit{Biosense Webster 首席软件工程师} 14 | \end{center} 15 | 16 | -------------------------------------------------------------------------------- /book/content/Preface.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 创建顶级软件并非易事。在线研究这个主题的开发者难以确定哪些建议是当前的,哪些方法已经更新,或已经有更好的实践方式。此外,大多数资源以混乱的方式解释过程,缺乏适当的背景、上下文和结构。 5 | 6 | 《Modern CMake for C++》提供了一个端到端的指南,通过全面处理C++解决方案的构建,提供了更简单的体验。它不仅教你如何在项目中使用CMake,还强调了如何使项目保持可维护性、优雅和简洁。该指南会引导你自动化完成许多项目中的常见任务,包括构建、测试和打包。 7 | 8 | 本书还会指导如何组织源目录、构建目标和创建包。随着了解的深入,将学习编译和链接可执行文件和库,详细理解这些过程,并优化每个步骤以获得最佳结果。此外,将发现如何将外部依赖项(如第三方库、测试框架、程序分析工具和文档生成器),进而整合到自己的项目中。最后,将学习如何导出、安装和打包解决方案,以供内部和外部使用。 9 | 10 | 完成这本书后,你将能以专业水平使用CMake。 11 | 12 | \mySubsectionNoFile{}{适读人群} 13 | 14 | 学会了C++之后,很快就会发现,仅仅掌握语言本身并不足以以最高标准交付项目。这本书填补了这一空白:它面向任何渴望成为更好的软件开发者,甚至专业构建工程师的人!如果想要从零开始学习现代CMake或提升和更新自己的CMake技能,阅读这本书吧。它将帮助你了解如何制作顶级的C++项目,并从其他构建环境中过渡。 15 | 16 | \mySubsectionNoFile{}{关于本书} 17 | 18 | 第1章,\textit{CMake入门},包括CMake的安装、命令行界面的使用,并介绍了CMake项目所需的基本构建块。 19 | 20 | 第2章,\textit{CMake语言},包括CCMake语言的基本概念,包括命令调用、参数、变量、控制结构和注释。 21 | 22 | 第3章,\textit{主流IDE中使用CMake},强调了集成开发环境(IDE)的重要性,指导读者选择IDE,并为Clion、Visual Studio Code和Visual Studio IDE为例,提供设置和说明。 23 | 24 | 第4章,\textit{开启第一个CMake项目},了解如何在顶层文件中配置基本的CMake项目,结构化文件树,并准备必要的工具链进行开发。 25 | 26 | 第5章,\textit{了解目标},探讨了逻辑构建目标的概念,理解它们的属性和不同类型,并学习如何为CMake项目自定义命令。 27 | 28 | 第6章,\textit{生成器表达式},解释了生成器表达式的目的和语法,包括如何使用它们进行条件扩展、查询和转换。 29 | 30 | 第7章,\textit{编译C++源文件},深入探讨了编译过程,配置预处理器和优化器,并了解减少构建时间和提高调试的技术。 31 | 32 | 第8章,\textit{链接可执行文件和库},理解链接机制,不同类型的库,单一定义规则,链接顺序,以及如何准备测试。 33 | 34 | 第9章,\textit{CMake中管理依赖项},了解如何管理第三方库,为那些缺乏CMake支持的库添加支持,并从互联网上获取外部依赖。 35 | 36 | 第10章,\textit{使用C++20模块},介绍了C++20模块,展示了如何在CMake中启用,并相应地配置工具链。 37 | 38 | 第11章,\textit{测试框架},理解自动化测试的重要性,利用CMake内置的测试支持,并使用主流框架开始单元测试。 39 | 40 | 第12章,\textit{程序分析工具},了解如何自动格式化源代码,并在构建时间和运行时检测软件错误。 41 | 42 | 第13章,\textit{生成文档},如何使用Doxygen从源代码自动创建文档,并添加样式以增强文档外观。 43 | 44 | 第14章,\textit{安装和打包},如何对项目进行发布,无论是否安装,创建可重用包,并为打包指定单个组件。 45 | 46 | 第15章,\textit{创建专业项目},应用本书所学到的所有知识来开发一个全面、专业级别的项目。 47 | 48 | 第16章,\textit{编写CMake预设},将高级项目配置封装到使用CMake预设文件的工作流程中,使项目设置和管理更加高效。 49 | 50 | 附录 - \textit{其他命令},作为与字符串、列表、文件和数学运算相关的各种CMake命令的参考。 51 | 52 | \mySubsectionNoFile{}{如何阅读} 53 | 54 | 本书假设你对C++和类Unix系统有基本的熟悉。尽管Unix知识不是严格的要求,但它将有助于理解本书中给出的示例。 55 | 56 | 本书的目标是CMake 3.26,但这里描述的大多数技术应该适用于CMake 3.15(之后添加的功能通常会突出显示)。有些章节已经更新到CMake 3.28,以涵盖最新功能。 57 | 58 | 运行示例的环境准备在第1-3章中介绍,如果熟悉这个工具,推荐使用本书提供的Docker镜像。 59 | 60 | \mySubsectionNoFile{}{下载示例} 61 | 62 | 本书的代码托管在GitHub上,地址为\url{https://github.com/PacktPublishing/Modern-CMake-for-Cpp-2E}。我们还有其他丰富的书籍和视频代码包,也可在GitHub中找到,地址为:\url{https://github.com/PacktPublishing/}。去看看吧! 63 | 64 | \mySubsectionNoFile{}{下载彩图} 65 | 66 | 我们还提供了一个PDF文件,其中包含了本书中使用的屏幕截图/图表的彩色图片。可以在这里下载: \url{https://packt.link/gbp/9781805121800} 67 | 68 | \mySubsectionNoFile{}{内容约定} 69 | 70 | 本书中使用了一些文本约定。 71 | 72 | 代码块如下设置: 73 | 74 | \begin{cmake} 75 | cmake_minimum_required(VERSION 3.26) 76 | project(Hello) 77 | add_executable(Hello hello.cpp) 78 | \end{cmake} 79 | 80 | 命令行输入或输出如下: 81 | 82 | \begin{shell} 83 | cmake --build --parallel [] 84 | cmake --build -j [] 85 | \end{shell} 86 | 87 | \begin{myNotic}{Note} 88 | 警告或重要注释 89 | \end{myNotic} 90 | 91 | \begin{myTip}{Tip} 92 | 提示和技巧 93 | \end{myTip} 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /book/content/Reviewers.tex: -------------------------------------------------------------------------------- 1 | \textbf{Eric Noulard}拥有法国ENSEEIHT的工程学位和法国UVSQ的计算机科学博士学位。Eric有着丰富的25年编写和编译各种语言源代码的经验。自2006年开始使用CMake,他也积极参与了CMake的发展。Eric曾为私营公司和政府机构服务。目前,他在Antidot工作,这是一家专注于语义搜索、人工智能和内容可访问性的软件供应商。Eric在研究团队中,负责将如生成性AI和先进NLP处理等新技术引入Antidot的旗舰产品Fluid Topics。 2 | 3 | \hspace*{\fill} 4 | \hspace*{\fill} 5 | 6 | \textbf{Giovanni Romano} 拥有28年的IT行业经验,从软件开发到应用程序/组件设计。目前他在Leica Geosystem AG担任高级软件工程师,专注于设计SDK、微服务和低延迟后端。作为Nokia/Blackberry Qt大使,他相信开源软件并致力于为该框架做出贡献。他的兴趣包括云原生应用程序、Kubernetes、Docker和GitOps。他喜欢使用C语言编程和打网球。 7 | 8 | -------------------------------------------------------------------------------- /book/content/chapter1/0.tex: -------------------------------------------------------------------------------- 1 | 2 | 软件开发的神奇之处在于,不仅是在创造能够运行的机制,还有创造解决方案的思想。 3 | 4 | 为了将想法变为现实,我们按照以下循环工作:设计、编码和测试。我们创造变化,并用编译器能理解的语言表达,继续检查其是否如期工作。要从源码中创建正确、高质量的软件,需要小心地重复执行容易出错的任务:调用正确的命令、检查语法、链接二进制文件、运行测试、报告问题等。 5 | 6 | 记住每个步骤需要付出巨大的努力。相反,我们希望更专注于编码,将其他事情委托给工具。理想情况下,这个过程会在更改代码后,通过一个按钮启动。这个过程应该是智能的、快速的、可扩展的,并且在不同操作系统和环境中的工作方式相同,应该得到多个集成开发环境(IDE)的支持。并且,可以将其简化为持续集成(CI)流水线,每次向仓库提交更改时,都会构建和测试软件。 7 | 8 | CMake是满足许多此类需求的答案,但要正确配置和使用也要花一些心思。CMake并不是复杂性的来源,复杂性来自于要处理的东西。别担心,我们将系统地学习整个过程,你会了解到软件构建是多么“简单”。 9 | 10 | 我知道你急于开始编写自己的CMake项目,这正是本书大部分内容中做的事情。但是,由于你将主要为了用户(包括你自己)创建项目,因此了解一下他们的视角对你来说很重要。 11 | 12 | 我们从这一点开始,逐步成为一个CMake高级用户。我们将介绍一些基础知识:这个工具是什么,工作原理,以及如何安装。然后,将深入探讨命令行和操作模式。最后,将总结项目文件的不同用途,并解释如何在完全不创建项目的情况下使用CMake。 13 | 14 | 本章中,将包括以下主题: 15 | 16 | \begin{itemize} 17 | \item 18 | 基础知识 19 | 20 | \item 21 | 安装CMake 22 | 23 | \item 24 | 掌握命令行 25 | 26 | \item 27 | 项目文件 28 | 29 | \item 30 | 脚本和Find-模块 31 | \end{itemize} 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /book/content/chapter1/1.tex: -------------------------------------------------------------------------------- 1 | 可以在这个章节的GitHub上找到出现的代码文件,地址为 \url{https://github.com/PacktPublishing/Modern-CMake-for-Cpp-2E/tree/main/examples/ch01}。 2 | 3 | 为了构建本书提供的示例,请执行推荐命令: 4 | 5 | \begin{shell} 6 | cmake -B -S 7 | cmake --build 8 | \end{shell} 9 | 10 | 请确保将占位符 替换为适当的路径。正如将在本章中学到的,是输出目录的路径,而是源代码所在的位置。 11 | 12 | 要构建C++程序,还需要适合的编译器。如果熟悉Docker,可以使用在“安装CMake”部分介绍的完全工具化的镜像。如果愿意手动设置CMake,我们将在同一部分介绍如何安装。 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /book/content/chapter1/3.tex: -------------------------------------------------------------------------------- 1 | 2 | CMake 是一个跨平台的、开源的软件,用 C++ 编写。我们可以自己编译它;然而,最好不要这样做。可以从官方网站 \url{https://cmake.org/download/} 下载预编译的二进制文件。 3 | 4 | 基于 Unix 的系统可以直接从命令行提供准备好的安装包。 5 | 6 | \begin{myNotic}{Note} 7 | CMake 并不包含编译器。如果系统还没有安装编译器,在使用 CMake 之前需要进行安装。确保将它们的执行文件路径添加到 PATH 环境变量中,这样 CMake 才能找到。 8 | 9 | 为了避免在学习本书时遇到工具和依赖问题,我建议通过第一种安装方法进行实践:Docker。在现实世界的场景中,你当然会想要使用本地的版本,除非你最初就在虚拟化的环境中。 10 | \end{myNotic} 11 | 12 | 来看看 CMake 可以使用的环境。 13 | 14 | \mySubsubsection{1.3.1}{Docker} 15 | 16 | Docker (\url{https://www.docker.com/}) 是一个跨平台的工具,提供操作系统级别的虚拟化,允许应用程序以定义良好的包形式(称为容器)进行传输。这些是自给自足的捆绑包,包含运行所需的所有库、依赖项和工具。Docker 在轻量级环境中执行其容器,彼此隔离。 17 | 18 | 这个概念使得分享完成特定过程所需的所有工具链变得极为方便,无需担心微小的环境差异。 19 | 20 | Docker 平台有一个公共的容器镜像仓库,\url{https://registry.hub.docker.com/},提供数百万个现成的镜像。 21 | 22 | 方便起见,我发布了两个 Docker 镜像: 23 | 24 | \begin{itemize} 25 | \item 26 | swidzinski/cmake2:base: 基于 Ubuntu 的镜像,包含构建时所需的精心挑选的工具和依赖项 27 | 28 | \item 29 | swidzinski/cmake2:examples: 基于上述工具链的镜像,包含本书中的所有项目和示例 30 | \end{itemize} 31 | 32 | 第一个选项,是为那些想有一个干净的镜像来构建项目的读者准备,第二个选项是为我们在章节中进行示例实践而准备。 33 | 34 | 可以按照官方文档中的说明安装 Docker(请参考 docs.docker.com/get-docker)。然后,在终端中执行以下命令来下载镜像,并启动容器: 35 | 36 | \begin{shell} 37 | $ docker pull swidzinski/cmake2:examples 38 | $ docker run -it swidzinski/cmake2:examples 39 | root@b55e271a85b2:root@b55e271a85b2:# 40 | \end{shell} 41 | 42 | 请注意,示例位于以下格式的目录中 43 | 44 | \begin{shell} 45 | devuser/examples/examples/ch/- 46 | \end{shell} 47 | 48 | <N>和<M>分别是零填充的章节和示例编号(例如 01, 08, 和 12) 49 | 50 | \mySubsubsection{1.3.2}{Windows} 51 | 52 | Windows 上安装CMake非常简单——只需从官方网站下载适用于 32 位或 64 位的版本即可。还可以选择适用于 Windows Installer 的便携式 ZIP 或 MSI 包装包,它会将 CMake 的 bin 目录添加到 PATH 环境变量中(图 1.2),这样一来就可以在任何目录中使用它,而不会出现此类错误: 53 | 54 | \begin{shell} 55 | 'cmake' 不是内部或外部命令,也不是可运行的程序或批处理文件。 56 | \end{shell} 57 | 58 | 如果选择 ZIP 包,需要手动安装。MSI 安装程序带有方便的 GUI: 59 | 60 | \myGraphic{0.8}{content/chapter1/images/2.png}{图 1.2:安装向导可以设置环境变量 PATH} 61 | 62 | 这是一个开源软件,所以可以自己构建 CMake。然而,在 Windows 上,首先需要在系统上获取 CMake 的二进制文件,从而生成新版本的CMake。 63 | 64 | Windows 平台与其他平台没有什么不同,也需要一个构建工具来完成由 CMake 开始构建过程。这里的一个流行选择是 Visual Studio IDE,它附带了 C++ 编译器。社区版可以从 Microsoft 的网站免费获得:\url{https://visualstudio.microsoft.com/downloads/}. 65 | 66 | \mySubsubsection{1.3.3}{Linux} 67 | 68 | Linux 上安装 CMake 的过程与安装其他软件包相同:命令行调用包管理器。包仓库通常会更新到 CMake 的最新版本,但通常不是最新版本。如果满意于此,并且使用像 Debian 或 Ubuntu 这样的发行版,最简单的方法就是直接安装适当的包: 69 | 70 | \begin{shell} 71 | $ sudo apt-get install cmake 72 | \end{shell} 73 | 74 | 对于 Red Hat 发行版,使用以下命令: 75 | 76 | \begin{shell} 77 | $ yum install cmake 78 | \end{shell} 79 | 80 | \begin{myTip}{Tip} 81 | 当安装包时,包管理器将获取为操作系统配置的仓库中可用的最新版本。在许多情况下,包仓库不提供最新版本,而是提供经过时间考验的稳定版本,以确保可靠的工作。根据需求选择,但旧版本可能不包含本书中描述的某些功能。 82 | \end{myTip} 83 | 84 | 要获取最新版本,请参考官方 CMake 网站的下载部分。如果知道当前版本号,可以使用以下命令。 85 | 86 | Linux x86\_64 的命令: 87 | 88 | \begin{shell} 89 | $ VER=3.26.0 && wget https://github.com/Kitware/CMake/releases/download/ 90 | v$VER/cmake-$VER-linux-x86_64.sh && chmod +x cmake-$VER-linux-x86_64.sh && 91 | ./cmake-$VER-linux-x86_64.sh 92 | \end{shell} 93 | 94 | Linux AArch64 的命令: 95 | 96 | \begin{shell} 97 | $ VER=3.26.0 && wget https://github.com/Kitware/CMake/releases/download/ 98 | v$VER/cmake-$VER-Linux-aarch64.sh && chmod +x cmake-$VER-Linux-aarch64.sh 99 | && ./cmake-$VER-Linux-aarch64.sh 100 | \end{shell} 101 | 102 | 或者,查看从源代码构建部分,了解如何相应的平台上自行编译 CMake。 103 | 104 | \mySubsubsection{1.3.4}{macOS} 105 | 106 | 这个平台也得到了 CMake 开发者的强烈支持。最流行的安装方法是通过 MacPorts: 107 | 108 | \begin{shell} 109 | $ sudo port install cmake 110 | \end{shell} 111 | 112 | 撰写本文时,MacPorts 中可用的最新版本是 3.24.4。要获取最新版本,请安装 cmake-devel 包: 113 | 114 | \begin{shell} 115 | $ sudo port install cmake-devel 116 | \end{shell} 117 | 118 | 或者,使用 Homebrew 包管理器: 119 | 120 | \begin{shell} 121 | $ brew install cmake 122 | \end{shell} 123 | 124 | macOS 包管理器将涵盖所有必要步骤,除非从源代码构建,否则无法获得最新版本。 125 | 126 | \mySubsubsection{1.3.5}{从源代码构建} 127 | 128 | 如果使用其他平台,或者只是想体验尚未发布(或被你最喜欢的包仓库采用)的最新构建,请从官方网站下载源代码并自行编译: 129 | 130 | \begin{shell} 131 | $ wget https://github.com/Kitware/CMake/releases/ 132 | download/v3.26.0/cmake-3.26.0.tar.gz 133 | $ tar xzf cmake-3.26.0.tar.gz 134 | $ cd cmake-3.26.0 135 | $ ./bootstrap 136 | $ make 137 | $ make install 138 | \end{shell} 139 | 140 | 从源代码构建相对较慢且需要更多步骤,没有其他方法可以自由选择 CMake 的版本。这对于系统包仓库中的包陈旧时特别有用:系统版本越旧,获得的更新就越少。 141 | 142 | 现在我们已经安装了 CMake,来看看怎么使用它吧! 143 | 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /book/content/chapter1/6.tex: -------------------------------------------------------------------------------- 1 | CMake 主要是专注于构建项目以产生其他系统(如 CI/CD 和测试平台,或部署到机器上或存储在工件仓库中)所消耗的工件。然而,有两种其他概念也使用 CMake 语言:脚本和模块。让我们介绍一下它们是什么,以及它们之间的区别。 2 | 3 | \mySubsubsection{1.6.1}{脚本} 4 | 5 | 6 | CMake 提供了一种与平台无关的编程语言,并附带了许多有用的命令。用这种语言编写的脚本可以与更大的项目捆绑在一起,也可以完全独立。 7 | 8 | 将其视为一种一致的跨平台工作方式。通常,为了执行任务,需要为 Linux 创建一个单独的 Bash 脚本,为 Windows 创建单独的批处理文件或 PowerShell 脚本等。CMake 抽象了这些,可以使用一个文件在所有平台上正常工作。当然,可以使用 Python、Perl 或 Ruby 等外部工具的脚本,但这会增加依赖性,并增加 C/C++ 项目的复杂性。既然大多数时候可以使用更简单的东西来完成工作,为什么要引入另一种语言呢?使用 CMake! 9 | 10 | 可以使用 -P 选项执行脚本:\textit{cmake -P 脚本.cmake}。 11 | 12 | 但使用脚本文件的实际要求是什么呢?并不多:脚本可以很复杂,或者只是一个空文件。不过,仍然建议在每一个脚本的开头调用 cmake\_minimum\_required() 命令,以告诉 CMake 应该对这个项目后续的命令应用哪些策略。 13 | 14 | 以下是一个简单脚本的例子: 15 | 16 | \filename{ch01/02-script/script�cmake} 17 | 18 | \begin{cmake} 19 | # An example of a script 20 | cmake_minimum_required(VERSION 3.26.0) 21 | message("Hello world") 22 | file(WRITE Hello.txt "I am writing to a file") 23 | \end{cmake} 24 | 25 | 运行脚本时,CMake 不会执行常规阶段(如配置或生成),也不会使用缓存,脚本中没有源代码树或构建树的概念。所以,在脚本模式下,项目特定的 CMake 命令不可用/不可使用。 26 | 27 | \mySubsubsection{1.6.2}{工具模块} 28 | 29 | CMake 项目可以使用外部模块来增强其功能。模块是用 CMake 语言编写的,包含宏定义、变量和执行各种功能的命令。它们从相当复杂的脚本(如 CPack 和 CTest 提供的脚本)到相对简单的脚本,如 AddFileDependencies 或 TestBigEndian。 30 | 31 | CMake 发行版打包了超过 80 个不同的实用模块。如果这还不够,可以通过浏览精选列表(如 \url{https://github.com/onqtam/awesome-cmake})从互联网上下载更多,或者自己编写模块。 32 | 33 | 要使用工具模块,需要 include() 命令。以下是展示这一操作的项目示例: 34 | 35 | \filename{ch01/03-module/CMakeLists。txt} 36 | 37 | \begin{cmake} 38 | cmake_minimum_required(VERSION 3.26.0) 39 | project(ModuleExample) 40 | include (TestBigEndian) 41 | test_big_endian(IS_BIG_ENDIAN) 42 | if(IS_BIG_ENDIAN) 43 | message("BIG_ENDIAN") 44 | else() 45 | message("LITTLE_ENDIAN") 46 | endif() 47 | \end{cmake} 48 | 49 | 我们将在它们与主题相关时,可以了解哪些模块可用。如果非常好奇,可以在 \url{https://cmake.org/cmake/help/latest/manual/cmake-modules.7.html} 找到打包模块的完整列表。 50 | 51 | \mySubsubsection{1.6.3}{Find-模块} 52 | 53 | 在“包定义文件”部分,我提到了 CMake 有一种机制来查找属于不支持 CMake ,且不提供 CMake 包配置文件的 external 依赖项的文件。这就是 Find-模块的作用。CMake 提供了超过 150 个 Find-模块,能够定位在系统上安装的这些包。与实用模块一样,网上还有更多的 Find-模块可用。 54 | 55 | 可以通过调用 find\_package() 命令,并提供相关包的名称进行使用。这样的 Find-模块将玩一个捉迷藏游戏,并检查它所寻找的软件的所有已知位置。如果找到了文件,将定义包含其路径的变量(如该模块手册中所述)。现在,CMake 可以针对该依赖项进行构建。 56 | 57 | 例如,FindCURL 模块搜索流行的客户端 URL 库,并定义以下变量:CURL\_FOUND, CURL\_INCLUDE\_DIRS, CURL\_LIBRARIES, 和 CURL\_VERSION\_STRING。 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /book/content/chapter1/7.tex: -------------------------------------------------------------------------------- 1 | 现在已经了解了 CMake 是什么,以及它是如何工作的;了解了 CMake 工具家族的关键组成部分,以及它在各种系统上的安装方式。了解了所有通过命令行运行 CMake 的方式:构建系统生成、构建项目、安装、运行脚本、命令行工具和打印帮助。知道了 CTest、CPack 和 GUI 应用程序。这将帮助你,以正确的视角为用户和其他开发者创建项目。此外,了解了项目由什么组成:目录、列表文件、配置、预设和帮助文件,以及在版本控制系统中应该忽略的内容。最后,知晓了其他非项目文件:独立的脚本和两种类型的模块——工具模块和 Find-模块。 2 | 3 | 下一章中,我们将了解如何使用 CMake 编程语言。将编写自己的列表文件,并为编写第一个脚本、项目和模块打开大门。 4 | -------------------------------------------------------------------------------- /book/content/chapter1/8.tex: -------------------------------------------------------------------------------- 1 | 2 | 需要获取更多信息,可以参考以下资料: 3 | 4 | \begin{itemize} 5 | \item 6 | 官方 CMake 网页和文档: 7 | 8 | \url{https://cmake.org/} 9 | 10 | \item 11 | 单配置生成器: 12 | 13 | \url{https://cgold.readthedocs.io/en/latest/glossary/single-config.html} 14 | 15 | \item 16 | CMake GUI 中的阶段分离: 17 | 18 | \url{https://stackoverflow.com/questions/39401003/} 19 | \end{itemize} -------------------------------------------------------------------------------- /book/content/chapter1/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter1/images/1.png -------------------------------------------------------------------------------- /book/content/chapter1/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter1/images/2.png -------------------------------------------------------------------------------- /book/content/chapter1/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter1/images/3.png -------------------------------------------------------------------------------- /book/content/chapter1/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter1/images/4.png -------------------------------------------------------------------------------- /book/content/chapter10/0.tex: -------------------------------------------------------------------------------- 1 | C++20 引入了一个新特性:模块,可以用模块文件替代了头文件中的纯文本符号声明,该模块文件将预编译为二进制格式,减少了构建时间。 2 | 3 | 我们将讨论 CMake 中 C++20 模块的内容,从 C++20 模块作为一个概念开始:相对于标准头文件的优点,以及如何简化源码单元的管理。尽管简化构建过程令人兴奋,但本章强调了其采纳的道路既困难又漫长。 4 | 5 | 理论部分结束后,我们将继续讨论在项目中实现模块的实际方面:将讨论在早期版本的 CMake 中启用它们的实验性支持,以及在 CMake 3.28 中的完整发布。 6 | 7 | 通过 C++20 模块的旅程不仅仅是为了理解一个新特性 —— 是关于重新思考在大型 C++ 项目中组件如何交互。本章结束时,不仅会了解模块的理论知识,还能通过示例获得实践经验,增强利用该特性实现更好的项目成果。 8 | 9 | 本章中,包含以下内容: 10 | 11 | \begin{itemize} 12 | \item 13 | C++20 模块是什么? 14 | 15 | \item 16 | 使用 C++20 模块支持的编写项目 17 | 18 | \item 19 | 配置工具链 20 | \end{itemize} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /book/content/chapter10/1.tex: -------------------------------------------------------------------------------- 1 | 可以在GitHub上的\url{https://github.com/PacktPublishing/Modern-CMake-for-Cpp-2E/tree/main/examples/ch10}找到本章中出现的代码文件。 2 | 3 | 尝试本章中的示例,需要以下工具链: 4 | 5 | \begin{itemize} 6 | \item 7 | CMake 3.26或更新版本(推荐3.28) 8 | 9 | \item 10 | 生成器: 11 | \begin{itemize} 12 | \item 13 | Ninja 1.11及更新版本(Ninja和Ninja Multi-Config) 14 | 15 | \item 16 | Visual Studio 17 2022及更新版本 17 | \end{itemize} 18 | 19 | \item 20 | 编译器: 21 | \begin{itemize} 22 | \item 23 | MSVC工具集14.34及更新版本 24 | 25 | \item 26 | Clang 16及更新版本 27 | 28 | \item 29 | GCC 14(针对2023年9月20日之后的开发分支)及更新版本 30 | \end{itemize} 31 | \end{itemize} 32 | 33 | 如果熟悉Docker,可以使用第1章中引入的全套工具镜像。 34 | 35 | 要构建本章提供的示例,请使用以下命令: 36 | 37 | \begin{shell} 38 | cmake -B <build tree> -S <source tree> -G "Ninja" -D CMAKE_CXX_COMPILER=clang++-18 && cmake --build <build tree> 39 | \end{shell} 40 | 41 | 请确保将占位符<build tree>和<source tree>替换为适当的路径。 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /book/content/chapter10/2.tex: -------------------------------------------------------------------------------- 1 | 三年前有写过关于如何使用C++模块文章,尽管模块已经为C++20规范的一部分,但C++生态系统的支持仍然没有准备好适配这个特性。幸运的是,自从本书的第一版以来,很多事情都发生了变化,随着CMake 3.28的发布,C++20模块得到了正式支持(尽管从3.26版本开始就已经有了实验性支持)。 2 | 3 | 三年似乎实现一个特性是很长的时间,但必须要记住,这不只是取决于CMake。许多碎片必须汇集在一起并良好工作。首先,需要编译器理解如何处理模块,然后构建系统如GNU Make或Ninja必须能够与模块一起工作,只有这样CMake才能使用这些新机制来提供对模块的支持。 4 | 5 | 这说明,并不是每个人都会使用最新的兼容工具,即使是现在,当前的支持仍然处于早期阶段。这些限制使得模块不适合大多数人们。所以也许不要急于依赖它们,来构建生产级别的项目。 6 | 7 | 尽管如此,如果你是尖端解决方案的爱好者,那么将会得到一次享受!如果可以严格控制项目的构建环境,例如:使用专用机器或构建容器化(Docker等),可以在内部使用模块。只需小心行事,并理解该方式可能会有所不同。可能会有一个时刻,会因为一个工具的缺失或对特性的错误实现,而完全放弃模块。 8 | 9 | C++构建的上下文中,“模块”是一个非常重的词。我们之前在本书中讨论过CMake的模块:find模块、工具模块等。而C++模块与CMake模块无关,它是C++20版本中添加的语言的原生特性。 10 | 11 | 在其核心,一个C++模块是个单一源文件,将头文件和实现文件的功能封装成一个连贯的代码单元。其包括两个主要部分: 12 | 13 | \begin{itemize} 14 | \item 15 | 二进制模块接口(BMI)类似于头文件的目的,但它是二进制格式,当其他翻译单元使用时,会减少了重新编译的需求。 16 | 17 | \item 18 | 模块实现单元提供模块的实现、定义和内部细节。其内容不能从模块外部直接访问,有效地封装了实现细节。 19 | \end{itemize} 20 | 21 | 引入模块是为了减少编译时间,解决预处理器和传统头文件的一些问题。来看看在典型的传统项目中,多个翻译单元如何粘合在一起。 22 | 23 | \myGraphic{0.7}{content/chapter10/images/1.png}{图10.1:使用传统头文件的项目结构} 24 | 25 | 前面的图显示了预处理器如何遍历项目树来构建程序。正如第7章中,为了构建每个翻译单元,预处理器机械地将文件拼接在一起,所以会生成一个包含所有由预处理器展开(包含的头文件)的长文件。这样,main.cpp将先包含自己的源码,然后是lib.h、a.h、1.h和2.h的内容。只有然后编译器才会启动并开始解析每一个字符以生成二进制目标文件。这样做本身并没有错,直到我们意识到为了编译lib.cpp,main.cpp中包含的头文件必须重新编译。并且这种冗余会随着每个翻译单元的添加而增加。 26 | 27 | 传统头文件还有其他问题: 28 | 29 | \begin{itemize} 30 | \item 31 | 需要包含保护,忘记时会导致问题。 32 | 33 | \item 34 | 循环引用的符号需要前置声明。 35 | 36 | \item 37 | 对头文件进行小的更改,需要重新编译所有翻译单元。 38 | 39 | \item 40 | 预处理器宏可能难以调试和维护。 41 | \end{itemize} 42 | 43 | 模块解决了其中的许多问题,但一些问题仍然相关:模块与头文件一样,可以相互依赖。当一个模块导入另一个模块时,仍然需要按照正确的顺序编译它们,从最内层的模块开始。因为模块的尺寸往往要大得多,所以这通常不是一个大问题。许多情况下,整个库可以存储在单个模块中。 44 | 45 | 来看看模块在实际中如何编写和使用。这个简单的例子中,我们只返回两个参数的和: 46 | 47 | \filename{ch10/01-cxx-modules/math.cppm} 48 | 49 | \begin{cpp} 50 | export module math; 51 | export int add(int a, int b) { 52 | return a + b; 53 | } 54 | \end{cpp} 55 | 56 | 一条语句开始,就告诉程序的其余部分这确实是一个名为math的模块。然后跟着一个用export关键字指定为,可以从模块外部访问的常规函数定义。 57 | 58 | \begin{myNotic}{Note} 59 | 模块文件的扩展名与常规C++源代码的不同。这是一个约定俗成的问题,不应影响代码的处理方式。最好是基于将使用的工具链来选择扩展名: 60 | 61 | \begin{itemize} 62 | \item 63 | .ixx是MSVC的扩展名。 64 | 65 | \item 66 | .cppm是Clang的扩展名。 67 | 68 | \item 69 | .cxx是GCC的扩展名。 70 | \end{itemize} 71 | \end{myNotic} 72 | 73 | 要使用这个模块,需要在程序中导入: 74 | 75 | \filename{ch10/01-cxx-modules/main�cpp} 76 | 77 | \begin{cpp} 78 | import math; 79 | 80 | #include <iostream> 81 | 82 | int main() { 83 | std::cout << "Addition 2 + 2 = " << add(2, 2) << std::endl; 84 | return 0; 85 | } 86 | \end{cpp} 87 | 88 | 导入math语句足以将模块中导出的符号,直接引入主程序。现在可以在main()函数的主体中使用add()函数。从表面上看,模块与头文件非常相似。但是,若尝试像往常一样编写CMake列表文件,将无法成功地构建项目。那么,是时候介绍使用C++模块的必要步骤了。 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /book/content/chapter10/3.tex: -------------------------------------------------------------------------------- 1 | 2 | 本书主要讨论CMake 3.26,但CMake经常更新,版本3.28就在本章完成前就发布了。如果使用的是这个版本或更新的版本,可以通过将cmake\_minimum\_required()命令设置为VERSION 3.28.0来访使用最新功能。 3 | 4 | 另一方面,如果坚持使用旧版本,或者想要迎合可能尚未升级的更广泛的受众,需要启用实验性支持才能在CMake中使用C++20模块。 5 | 6 | 让我们探讨如何做到这一点。 7 | 8 | \mySubsubsection{10.3.1.}{CMake 3.26和3.27中启用实验性支持} 9 | 10 | 实验性支持代表一种协议:作为开发者,承认这个功能尚未准备好投入生产,应仅用于测试目的。要签署这样的协议,需要在项目的列表文件中,将CMAKE\_EXPERIMENTAL\_CXX\_MODULE\_CMAKE\_API变量设置为CMake版本的一个特定值。 11 | 12 | \begin{myNotic}{Note} 13 | CMake的官方Kitware仓库托管了一个问题跟踪器,可以在其中搜索标签area:cxxmodules。直到3.28版本发布,只报告了一个问题(在3.25.0中),这是潜在稳定功能的一个很好的指标。如果决定启用实验,请构建项目,以确认它适用于用户。 14 | \end{myNotic} 15 | 16 | 以下是在CMake的仓库和文档中可以找到的标志: 17 | 18 | \begin{itemize} 19 | \item 20 | 3c375311-a3c9-4396-a187-3227ef642046 对应 3.25 (无正式文件) 21 | 22 | \item 23 | 2182bf5c-ef0d-489a-91da-49dbc3090d2a 对应 3.26 24 | 25 | \item 26 | aa1f7df0-828a-4fcd-9afc-2dc80491aca7 对应 3.27 27 | \end{itemize} 28 | 29 | 如果不能访问CMake 3.25,那就头痛了,模块在该版本☞前不可用。此外,如果CMake版本早于3.27,还需要设置一个变量来为模块启用动态依赖: 30 | 31 | \begin{cmake} 32 | set(CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP 1) 33 | \end{cmake} 34 | 35 | 以下是如何为当前版本自动选择正确的API密钥,以及明确不支持版本构建(这个例子中,将只支持CMake 3.26及以上的版本)。 36 | 37 | \filename{ch10/01-cxx-modules/CMakeLists.txt} 38 | 39 | \begin{cmake} 40 | cmake_minimum_required(VERSION 3.26.0) 41 | project(CXXModules CXX) 42 | 43 | # turn on the experimental API 44 | if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.28.0) 45 | # Assume that C++ sources do import modules 46 | cmake_policy(SET CMP0155 NEW) 47 | elseif(CMAKE_VERSION VERSION_GREATER_EQUAL 3.27.0) 48 | set(CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API 49 | "aa1f7df0-828a-4fcd-9afc-2dc80491aca7") 50 | elseif(CMAKE_VERSION VERSION_GREATER_EQUAL 3.26.0) 51 | set(CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API 52 | "2182bf5c-ef0d-489a-91da-49dbc3090d2a") 53 | set(CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP 1) 54 | else() 55 | message(FATAL_ERROR "Version lower than 3.26 not supported") 56 | endif() 57 | \end{cmake} 58 | 59 | 逐条解释: 60 | 61 | \begin{enumerate} 62 | \item 63 | 首先,检查版本是否为3.28或更新。允许使用cmake\_policy()启用CMP0155策略。如果想支持3.28以下的版本,这是必需的。 64 | 65 | \item 66 | 如果不是这种情况,将检查版本是否高于3.27。如果是,将设置适当的API密钥。 67 | 68 | \item 69 | 如果不低于3.27,将检查它是否高于3.26。如果是这样,设置适当的API密钥,并启用实验性C++20模块动态依赖标志。 70 | 71 | \item 72 | 如果版本低于3.26,项目不支持,并将输出致命错误消息通知用户。 73 | \end{enumerate} 74 | 75 | 这使我们能够支持从3.26开始的CMake版本范围。如果有幸在每个将构建项目的环境中运行CMake 3.28,则上述if()块是不必要的。那么什么是必要的呢? 76 | 77 | \mySubsubsection{10.3.2.}{启用对CMake 3.28及以上的支持} 78 | 79 | 要从3.28开始使用C++20模块,必须明确声明这个版本为最低版本: 80 | 81 | \begin{cmake} 82 | cmake_minimum_required(VERSION 3.28.0) 83 | project(CXXModules CXX) 84 | \end{cmake} 85 | 86 | 如果将最低要求版本设置为3.28或以上,将默认启用CMP0155策略。继续阅读以了解在定义模块之前需要配置的东西。如果需要3.27或更低的版本,即使项目是使用CMake 3.28或更新的版本构建的,构建也可能会失败。 87 | 88 | 接下来要考虑的是编译器要求。 89 | 90 | \mySubsubsection{10.3.3.}{设置编译器要求} 91 | 92 | 无论是使用CMake 3.26、3.27、3.28还是更新的版本来构建,为了使用C++模块创建解决方案,都需要设置两个全局变量。第一个是禁用不受支持的C++扩展,第二个是确保编译器支持所需的标准。 93 | 94 | \filename{ch10/01-cxx-modules/CMakeLists.txt (续)} 95 | 96 | \begin{cmake} 97 | # Libc++ has no support compiler extensions for modules. 98 | set(CMAKE_CXX_EXTENSIONS OFF) 99 | set(CMAKE_CXX_STANDARD 20) 100 | \end{cmake} 101 | 102 | 由于支持模块的编译器数量非常有限,设置标准可能看起来有些多余。然而,这对于保护项目未来不受影响是一种良好的习惯。 103 | 104 | 总体配置相当简单,到此结束。我们现在可以在CMake中定义一个模块。 105 | 106 | \mySubsubsection{10.3.4.}{声明一个C++模块} 107 | 108 | CMake模块定义利用了target\_sources()命令和FILE\_SET关键字: 109 | 110 | \begin{cmake} 111 | target_sources(math 112 | PUBLIC FILE_SET CXX_MODULES TYPE CXX_MODULES FILES math.cppm 113 | ) 114 | \end{cmake} 115 | 116 | 这里,引入了一种新的文件集类型:CXX\_MODULES。这种类型默认情况下只在CMake 3.28及以后的版本中支持。对于3.26,需要启用实验性API。如果没有适当的支持,将会出现如下错误消息: 117 | 118 | \begin{shell} 119 | CMake Error at CMakeLists.txt:25 (target_sources): 120 | target_sources File set TYPE may only be "HEADERS" 121 | \end{shell} 122 | 123 | 如果在构建输出中看到这个,请检查代码是否正确。如果使用的版本的API密钥值不正确,也会出现这个消息。 124 | 125 | 如前所述,在经常使用的相同二进制文件中使用模块是有好处的。 126 | 127 | 但在创建库时,这些优势更为明显。这样的库可以在其他项目中使用,或者在同一项目中让其他库使用,从而进一步增强模块化。 128 | 129 | 要声明模块并将其与主程序链接,使用以下CMake配置: 130 | 131 | \filename{ch10/01-cxx-modules/CMakeLists.txt (continued)} 132 | 133 | \begin{cmake} 134 | add_library(math) 135 | target_sources(math 136 | PUBLIC FILE_SET CXX_MODULES FILES math.cppm 137 | ) 138 | target_compile_features(math PUBLIC cxx_std_20) 139 | set_target_properties(math PROPERTIES CXX_EXTENSIONS OFF) 140 | 141 | add_executable(main main.cpp) 142 | target_link_libraries(main PRIVATE math) 143 | \end{cmake} 144 | 145 | 为了确保这个库可以在其他项目中使用,必须使用target\_compile\_features()命令并明确要求cxx\_std\_20。此外,还需要在目标级别重复设置CXX\_EXTENSIONS OFF。如果没有这个,CMake将生成错误并停止构建。这似乎有些多余,可能会在CMake的未来版本中得到解决。 146 | 147 | 项目设置完成后,终于到了构建它的时候了。 148 | -------------------------------------------------------------------------------- /book/content/chapter10/4.tex: -------------------------------------------------------------------------------- 1 | 2 | 根据Kitware网站上的博客文章(见“扩展阅读”部分),CMake最早在3.25版本就支持模块功能。尽管3.28版本正式支持此功能,但这并不是我们要享受模块便利性的唯一拼图。 3 | 4 | 下一个要求集中在构建系统上:需要支持动态依赖。截至目前,只有两个选择: 5 | 6 | \begin{itemize} 7 | \item 8 | Ninja 1.11及更新版本(Ninja和Ninja Multi-Config) 9 | 10 | \item 11 | Visual Studio 17 2022及更新版本 12 | \end{itemize} 13 | 14 | 同样,编译器需要以特定格式生成映射源依赖的文件,以供CMake使用。这种格式在Kitware开发者撰写的一篇论文中有描述,这篇论文称为p1589r5。该论文已提交给所有主流编译器以供实施。目前,只有以下三种编译器实现了所需的格式: 15 | 16 | \begin{itemize} 17 | \item 18 | Clang 16 19 | 20 | \item 21 | Visual Studio 2022 17.4 (19.34)中的MSVC 22 | 23 | \item 24 | GCC 14(针对开发分支,2023年9月20日之后)及更新版本 25 | \end{itemize} 26 | 27 | 假设环境中有所有必要的工具(可以使用为本书提供的Docker镜像),并且Make项目已准备好构建,剩下的就是配置CMake以使用所需的工具链。 28 | 29 | \begin{shell} 30 | cmake -B <build tree> -S <source tree> -G "Ninja" 31 | \end{shell} 32 | 33 | 此命令将配置项目以使用Ninja构建系统。下一步是设置编译器。如果默认编译器不支持模块,并且已安装了另一个编译器来尝试,可以通过定义全局变量CMAKE\_CXX\_COMPILER来实现,如下所示: 34 | 35 | \begin{shell} 36 | cmake -B <build tree> -S <source tree> -G "Ninja" -D CMAKE_CXX_ COMPILER=clang++-18 37 | \end{shell} 38 | 39 | 我们选择Clang 18,因为它是撰写本文时(包含在Docker镜像中)可用的最新版本。成功配置后(会看到一些关于实验性功能的警告),需要构建项目: 40 | 41 | \begin{shell} 42 | cmake --build <build tree> 43 | \end{shell} 44 | 45 | 和往常一样,确保用适当的路径替换占位符<build tree>和<source tree>。如果一切顺利,可以运行程序,观察模块功能按预期工作: 46 | 47 | \begin{shell} 48 | $ ./main 49 | Addition 2 + 2 = 4 50 | \end{shell} 51 | 52 | 就这样,C++20模块在实际中得以应用。 53 | 54 | \begin{myNotic}{Note} 55 | “扩展阅读”部分包括来自Kitware的博客文章和关于C++编译器的源依赖格式的提案,提供了更多关于C++20模块部署和使用的深入见解。 56 | \end{myNotic} 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /book/content/chapter10/5.tex: -------------------------------------------------------------------------------- 1 | 本章中,我们深入探讨了 C++20 模块,了解了它们与 CMake 模块的区别,并代表了 C++ 在简化编译和解决与冗余头文件编译,及处理预处理器宏相关挑战方面的进步。 2 | 3 | 我们通过一个简单的示例演示了如何编写和导入 C++20 模块。然后,探讨了如何为 C++20 模块设置 CMake。由于这个特性是实验性的,需要设置特定的变量,提供了一系列条件语句来确保项目能够正确配置正在使用的 CMake 版本。关于必要的工具,我们强调构建系统必须支持动态依赖,目前的选择是 Ninja 1.11 或更新版本。对于编译器支持,Clang 16 和 Visual Studio 2022 17.4 (19.34) 中的 MSVC 适合完全支持 C++20 模块,而 GCC 的支持仍在等待中。此外,还指导各位通过配置 CMake 来使用选定的工具链,包括选择构建系统生成器和设置编译器版本。配置和构建项目后,可以运行程序来查看 C++20 模块的实际应用。 4 | 5 | 下一章中,我们将了解自动化测试的重要性及其应用,以及 CMake 对测试框架的支持。 -------------------------------------------------------------------------------- /book/content/chapter10/6.tex: -------------------------------------------------------------------------------- 1 | 2 | \begin{itemize} 3 | \item 4 | 描述新特性的博客文章 5 | 6 | \url{https://www.kitware.com/import-cmake-c20-modules/} 7 | 8 | \item 9 | 针对 C++ 编译器的建议依赖格式: 10 | 11 | \url{https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1689r5.html} 12 | \end{itemize} -------------------------------------------------------------------------------- /book/content/chapter10/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter10/images/1.png -------------------------------------------------------------------------------- /book/content/chapter11/0.tex: -------------------------------------------------------------------------------- 1 | 经验丰富的专业人士知道,测试必须自动化。这是几年前有人向他们解释的,或者通过艰难的方式学到的。对于没有经验的程序员来说,这一做法并不那么明显,看起来像是额外的、不必要的劳动,并不会带来太多价值。这是可以理解的:当某人刚开始编写代码时,还没有创建真正复杂的解决方案,也没有在大型的代码库上工作。很可能,是自己项目的唯一开发者。这些早期项目很少需要超过几个月就能完成,因此很难看到代码在较长时间内是如何恶化的。 2 | 3 | 所有这些因素都导致人们认为编写测试是浪费时间和精力。编程新手可能会告诉自己,每次进行构建和运行流程时,实际上确实在测试代码。毕竟,已经手动确认了代码能够正常工作,并做到了预期效果。所以,是时候继续下一个任务了,对吧?自动化测试确保新的更改不会无意中破坏程序。本章中,将学习为什么测试很重要,以及如何使用CTest来协调测试执行。CTest可以查询可用的测试,过滤执行,随机排序,重复执行,并设置时间限制。我们将探讨如何使用这些功能,控制CTest的输出,并处理测试失败。 4 | 5 | 接下来,将修改项目的结构以适应测试,并创建自己的测试运行器。 6 | 7 | 介绍了基本原理之后,将继续添加流行的测试框架:Catch2和GoogleTest(也称为GTest),以及其模拟库。最后,将介绍使用LCOV进行详细的测试覆盖率报告。 8 | 9 | 本章中,将包含以下内容: 10 | 11 | \begin{itemize} 12 | \item 13 | 为什么自动化测试值得麻烦? 14 | 15 | \item 16 | 使用CTest在CMake中标准化测试 17 | 18 | \item 19 | 为CTest创建最基本的单元测试 20 | 21 | \item 22 | 单元测试框架 23 | 24 | \item 25 | 生成测试覆盖率报告 26 | \end{itemize} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /book/content/chapter11/1.tex: -------------------------------------------------------------------------------- 1 | 可以在GitHub上的\url{https://github.com/PacktPublishing/Modern-CMake-for-Cpp-2E/tree/main/examples/ch11}找到本章中出现的代码文件。 2 | 3 | 为了构建本书提供的示例,请使用推荐的命令: 4 | 5 | \begin{shell} 6 | cmake -B <build tree> -S <source tree> 7 | cmake --build <build tree> 8 | \end{shell} 9 | 10 | 请确保用适当的路径替换占位符<build tree>和<source tree>。提醒一下,<build tree>是目标/输出目录的路径,而<source tree>是源码所在的位置。 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /book/content/chapter11/2.tex: -------------------------------------------------------------------------------- 1 | 一条生产线上,一台机器在钢板 上打孔。这些孔需要特定的大小和形状,以便为成品安装螺栓。生产线的设计者会设置好机器,测试这些孔,然后继续下一步。最终,某些事情会发生变化:钢材可能更厚,工人可能调整了孔的大小,或者因为设计变更需要打更多的孔。一个聪明的设计者会在关键点安装质量控制检查,以确保产品符合规格。孔是如何形成的并不重要:钻孔、冲孔,还是激光切割。 2 | 3 | 同样的原则也适用于软件开发。很难预测哪些代码能够多年保持稳定,哪些将经历多次修订。随着软件功能的扩展,必须确保不会无意中破坏已有的东西。我们也会犯错误。即使是最优秀的开发者也无法预见每一次更改的所有影响。开发者经常要处理他们最初并未编写的代码,可能并不了解所有背后的假设。他们会阅读代码,形成心理模型,进行更改,并希望一切顺利。当这种方式不奏效时,修复错误可能需要数小时或数天的时间,并且会对产品和用户产生负面影响。 4 | 5 | 有时候,会遇到难以理解的代码。甚至可能开始责怪别人造成了混乱,结果发现是自己造成的。这种情况通常发生在编写代码时过于匆忙,没有完全理解问题的情况下。 6 | 7 | 作为开发者,我们不仅受到项目截止日期或有限预算的压力;有时候还需要在夜间醒来修复关键问题。令人惊讶的是,一些不那么明显的错误是如何在代码审查中溜掉的呢? 8 | 9 | 自动化测试可以预防大多数这些问题。这些测试是代码片段,用于验证另一段代码的行为是否正确。顾名思义,每当有人进行更改时,这些测试会自动运行,通常作为构建过程的一部分。它们通常为一个步骤,以确保在将代码合并到仓库之前,保证代码的质量。 10 | 11 | 有人可能会为了节省时间而跳过创建自动化测试,但这将是一个代价高昂的错误。正如史蒂文·赖特所说:“经验是在你真正需要之后才获得的东西。”除非正在编写一次性脚本或进行实验,否则不要跳过测试。可能会因为精心编写的代码不断测试失败而感到沮丧,但一个失败的测试意味着刚刚避免在生产环境中引入一个重大问题。现在花在测试上的时间,将节省以后在修复错误上的时间——晚上能睡得更香。并且,测试也不是难以添加和维护东西。 -------------------------------------------------------------------------------- /book/content/chapter11/5.tex: -------------------------------------------------------------------------------- 1 | C++ 具备有限的内省能力,但无法提供像 Java 那样强大的反射特性。这可能是为什么在 C++ 代码中编写测试和单元测试框架,比在其他功能更丰富的环境中更具挑战性的原因。这种有限方法的一个结果是,开发者需要更深入地参与编写可测试的代码。需要仔细设计接口,并考虑实际应用。例如,如何避免编译代码两次,并在测试和生产之间重用工件? 2 | 3 | 对于较小的项目来说,编译时间可能不是大问题,但随着项目的发展,缩短编译循环的需求仍然存在。前面的例子中,将所有 SUT 源文件包含在单元测试可执行文件中,除了 main.cpp 文件。如果仔细观察,会注意到该文件中的一些代码没有测试(即 main() 函数本身的内容)。编译代码两次会引入一个轻微的风险,即产生的工件可能不会完全相同。随着时间的推移,这些差异可能会逐渐增加,特别是在添加编译标志和预处理器指令时,如果贡献者急于完成、经验不足或不熟悉项目,这可能会带来风险。 4 | 5 | 这个问题有多种解决方案,但最直接的方法是将整个解决方案构建为一个库,并与单元测试链接。可能会有人想知道然后如何运行。那么就创建一个引导可执行文件,它与库链接并执行其代码。 6 | 7 | 首先,将当前的 main() 函数重命名为 run() 或 start\_program()。然后,创建一个只包含新的 main() 函数的实现文件(bootstrap.cpp)。这个函数充当适配器:唯一作用是提供一个入口点并调用 run(),传递命令行参数。将所有东西链接在一起后,最终会得到一个可测试的项目。 8 | 9 | 通过重命名 main(),现在可以将 SUT 与测试链接,并测试其 main 功能。否则,会违反第 8 章讨论的“单一定义规则”(ODR),测试运行器也需要自己的 main() 函数。 10 | 11 | 注意,测试框架默认提供它自己的 main() 函数,它会自动检测所有链接的测试,并根据配置运行它们。 12 | 13 | 这种方法产生的工件可以分为以下目标: 14 | 15 | \begin{itemize} 16 | \item 17 | 包含生产代码的 sut 库 18 | 19 | \item 20 | 引导程序,其中包含调用 sut 中 run() 的 main() 包装器 21 | 22 | \item 23 | 单元测试,其中包含运行所有 sut 测试的 main() 包装器 24 | \end{itemize} 25 | 26 | 下面的图表显示了目标之间的符号关系: 27 | 28 | \myGraphic{0.9}{content/chapter11/images/1.png}{图 11.1:测试和生产可执行文件之间共享工件} 29 | 30 | 最终得到六个实现文件,分别产生各自的(.o)对象文件: 31 | 32 | \begin{itemize} 33 | \item 34 | calc.cpp: 将要进行单元测试的 Calc 类。这称为单元测试对象(UUT),因为 UUT 是 SUT 的一个特化。 35 | 36 | \item 37 | run.cpp: 原始入口点重命名为 run(),现在可以对其进行测试。 38 | 39 | \item 40 | bootstrap.cpp: 新的 main() 入口点,调用 run()。 41 | 42 | \item 43 | calc\_test.cpp: 测试 Calc 类。 44 | 45 | \item 46 | run\_test.cpp: 新的 run() 测试可以放在这里。 47 | 48 | \item 49 | unit\_tests.o: 单元测试的入口点,扩展为调用 run() 的测试。 50 | \end{itemize} 51 | 52 | 我们即将构建的库不一定是静态或共享库。通过选择对象库,可以避免不必要的归档或链接。从技术上讲,使用动态链接 SUT 可以节省一些时间,但经常发现自己同时修改两个目标:测试和 SUT,这抵消了节省的时间。 53 | 54 | 让我们看看之前名为 main.cpp 的文件是如何变化的: 55 | 56 | \filename{ch11/02-structured/src/run.cpp} 57 | 58 | \begin{cpp} 59 | #include <iostream> 60 | #include "calc.h" 61 | using namespace std; 62 | int run() { 63 | Calc c; 64 | cout << "2 + 2 = " << c.Sum(2, 2) << endl; 65 | cout << "3 * 3 = " << c.Multiply(3, 3) << endl; 66 | return 0; 67 | } 68 | \end{cpp} 69 | 70 | 变化很小:文件和函数重命名,添加了一个返回语句,因为编译器不会隐式地为 main() 之外的函数添加返回语句。 71 | 72 | 新的 main() 函数如下所示: 73 | 74 | \filename{ch11/02-structured/src/bootstrap.cpp} 75 | 76 | \begin{cpp} 77 | int run(); // declaration 78 | int main() { 79 | run(); 80 | } 81 | \end{cpp} 82 | 83 | 保持简单,我们声明链接器将提供来自另一个翻译单元的 run() 函数,并调用它。 84 | 85 | 接下来是 src 下的列表文件: 86 | 87 | \filename{ch11/02-structured/src/CMakeLists.txt} 88 | 89 | \begin{cmake} 90 | add_library(sut STATIC calc.cpp run.cpp) 91 | target_include_directories(sut PUBLIC .) 92 | add_executable(bootstrap bootstrap.cpp) 93 | target_link_libraries(bootstrap PRIVATE sut) 94 | \end{cmake} 95 | 96 | 首先,创建一个 SUT 库,并将 . 标记为 PUBLIC 包含目录,这样它就会传播到所有与 SUT 链接的目标(即 bootstrap 和 unit\_tests)。请注意,包含目录相对于列表文件,允许使用点(.)来引用当前的 <source\_tree>/src 目录。 97 | 98 | 现在是更新 unit\_tests 目标的时候了。我们将替换对 …/src/calc.cpp 文件的直接引用,改为对 sut 的链接引用,用于 unit\_tests 目标,还将为 run\_test.cpp 文件中的主函数添加一个新的测试。为了简洁,我们将省略对此的讨论,但如果感兴趣,可以查看本书仓库中的示例。 99 | 100 | 同时,以下是整个测试列表文件: 101 | 102 | \filename{ch11/02-structured/test/CMakeLists.txt} 103 | 104 | \begin{cmake} 105 | add_executable(unit_tests 106 | unit_tests.cpp 107 | calc_test.cpp 108 | run_test.cpp) 109 | target_link_libraries(unit_tests PRIVATE sut) 110 | add_test(NAME SumAddsTwoInts COMMAND unit_tests 1) 111 | add_test(NAME MultiplyMultipliesTwoInts COMMAND unit_tests 2) 112 | add_test(NAME RunOutputsCorrectEquations COMMAND unit_tests 3) 113 | \end{cmake} 114 | 115 | 完成了!我们按需注册了新的测试。通过遵循这种做法,可以确保测试是在用于生产中的机器代码上执行的。 116 | 117 | \begin{myNotic}{Note} 118 | 这里使用的目标名称,sut 和 bootstrap,是为了从测试的角度清楚地表明它们是关于什么的。在实际项目中,应该选择与生产代码上下文(而不是测试)匹配的名称。例如,对于一个 FooApp,将目标命名为 foo 而不是 bootstrap,将 lib\_foo 而不是 sut。 119 | \end{myNotic} 120 | 121 | 现在,我们知道了如何在适当的目标中构建一个可测试的项目,再将焦点转移到测试框架本身。我们不想手动将每个测试案例添加到列表文件中,对吧? 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /book/content/chapter11/8.tex: -------------------------------------------------------------------------------- 1 | 表面上,与恰当测试相关的复杂性似乎如此之大,以至于它们不值得付出努力。有多少代码在外运行却没有任何测试,这真是令人惊讶,主要是测试软件是一项令人生畏的任务。如果是手动进行,则更是如此。不幸的是,如果没有严格的自动化测试,代码中的问题都是不完整或根本不可见的。未经测试的代码可能更快编写(但并非总是如此);然而,要阅读、重构和修复这些代码,绝对要慢得多。 2 | 3 | 本章中,概述了从一开始就进行测试工作的几个关键原因,其中最引人注目的是心理健康和良好的夜间睡眠。没有一个开发者躺在床上想:等不及几个小时后被打扰,去处理一些生产环境中的火灾和修复错误。但认真地说,在将错误部署到生产环境之前捕捉它们,对你(和公司)可能是一根救命稻草。 4 | 5 | 当涉及到测试工具时,CMake在这里真正展现了它的强大。CTest在检测故障测试方面能发挥奇迹:隔离、混洗、重复和超时。所有这些技术都非常有用,可以通过一个方便的命令行标志获得。我们了解到如何使用CTest来列出测试,过滤,并控制测试用例的输出,但最重要的是,现在知道如何使用标准解决方案的真正力量。任何用CMake构建的项目都可以进行完全相同的测试,而无需探究其内部细节。 6 | 7 | 接下来,我们结构化了项目,简化了测试过程,并在生产代码和测试运行器之间重用相同的对象文件。编写自己的测试运行器很有趣,但也许我们应专注于我们程序实际应解决的问题,并花时间接受一个主流的第三方测试框架。 8 | 9 | 说到这,我们学习了Catch2和GoogleTest的基础知识。进一步深入研究了GMock库的细节,并理解了测试替身,如何工作以实现真正的单元测试。最后,使用LCOV设置了一些报告。毕竟,没有什么比硬数据更能证明,我们的解决方案确实是经过全面测试的。 10 | 11 | 下一章中,将讨论更多有用的工具,以提高代码的质量,并找出潜在的问题。 -------------------------------------------------------------------------------- /book/content/chapter11/9.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | \begin{itemize} 4 | \item 5 | 关于CTest的CMake文档: 6 | 7 | \url{https://cmake.org/cmake/help/latest/manual/ctest.1.html} 8 | 9 | \item 10 | Catch2文档: 11 | 12 | \url{https://github.com/catchorg/Catch2/blob/devel/docs/} 13 | 14 | \item 15 | GMock教程: 16 | 17 | \url{https://google.github.io/googletest/gmock_for_dummies.html} 18 | 19 | \item 20 | Abseil: 21 | 22 | \url{https://abseil.io/} 23 | 24 | \item 25 | 使用Abseil的实时更新: 26 | 27 | \url{https://abseil.io/about/philosophy#we-recommend-that-you-choose-to-liveat-head} 28 | 29 | \item 30 | 为什么Abseil会成为GTest的依赖项: 31 | 32 | \url{https://github.com/google/googletest/issues/2883} 33 | 34 | \item 35 | GCC中的覆盖率: 36 | 37 | \url{https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html} 38 | 39 | \url{https://gcc.gnu.org/onlinedocs/gcc/Invoking-Gcov.html} 40 | 41 | \url{https://gcc.gnu.org/onlinedocs/gcc/Gcov-Data-Files.html} 42 | 43 | \item 44 | Clang中的覆盖率: 45 | 46 | \url{https://clang.llvm.org/docs/SourceBasedCodeCoverage.html} 47 | 48 | \item 49 | LCOV命令行工具的文档: 50 | 51 | \url{https://helpmanual.io/man1/lcov/} 52 | 53 | \item 54 | LCOV项目仓库: 55 | 56 | \url{https://github.com/linux-test-project/lcov} 57 | 58 | \item 59 | GCOV更新功能: 60 | 61 | \url{https://gcc.gnu.org/onlinedocs/gcc/Invoking-Gcov.html#Invoking-Gcov} 62 | \end{itemize} 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /book/content/chapter11/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter11/images/1.png -------------------------------------------------------------------------------- /book/content/chapter11/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter11/images/2.png -------------------------------------------------------------------------------- /book/content/chapter11/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter11/images/3.png -------------------------------------------------------------------------------- /book/content/chapter12/0.tex: -------------------------------------------------------------------------------- 1 | 编写高质量的代码并非易事,即使对于经验丰富的开发者来说也是如此。通过在解决方案中包含测试,降低了在主代码中犯基本错误的可能性,但这还不足以避免更复杂的问题。每一款软件都包含如此多的细节,跟踪它们可以成为一份全职工作。各种约定和特定的设计实践由负责维护产品的团队建立。 2 | 3 | 有些问题与一致的编码风格相关:代码应该使用80列还是120列?应该允许使用std::bind,还是坚持使用lambda函数?使用C风格数组是否可以接受?应该将小函数写在一行中吗?应该总是使用auto,还是只在它提高可读性时使用?理想情况下,应该避免使用公认的通常不正确的语句:无限循环、使用标准库保留的标识符、无意中的数据丢失、不必要的if语句,以及不是“最佳实践”的东西(更多信息请参见“扩展阅读”部分)。 4 | 5 | 另一个需要考虑的方面是代码现代化。随着C++的发展,引入了新特性,以了解更新到最新标准的挑战性。此外,手动执行此操作既耗时又增加了引入错误的风险,特别是在大型代码库中。最后,应该检查事物在运行时的操作情况:运行程序并检查其内存。内存使用后是否正确释放?是否正在访问正确初始化的数据?还是代码试图访问不存在的指针? 6 | 7 | 手动管理所有这些挑战和问题既耗时又容易出错。幸运的是,可以使用自动化工具来检查和执行规则,纠正错误,并使代码保持最新。是时候探索程序分析工具了。代码将在每次构建时受到严格审查,以确保其符合行业标准。 8 | 9 | 本章中,将包含以下内容: 10 | 11 | \begin{itemize} 12 | \item 13 | 执行格式化 14 | 15 | \item 16 | 使用静态检查器 17 | 18 | \item 19 | 使用Valgrind进行动态分析 20 | \end{itemize} -------------------------------------------------------------------------------- /book/content/chapter12/1.tex: -------------------------------------------------------------------------------- 1 | 可以在GitHub上找到本章中出现的代码文件,地址为 \url{https://github.com/PacktPublishing/Modern-CMake-for-Cpp-2E/tree/main/examples/ch12}。 2 | 3 | 为了构建本书提供的示例,请使用以下推荐的命令: 4 | 5 | \begin{shell} 6 | cmake -B <build tree> -S <source tree> 7 | cmake --build <build tree> 8 | \end{shell} 9 | 10 | 请确保将占位符<build tree>和<source tree>替换为适当的路径。提醒一下,<build tree>是目标/输出目录的路径,而<source tree是存放源码的路径。 11 | 12 | -------------------------------------------------------------------------------- /book/content/chapter12/2.tex: -------------------------------------------------------------------------------- 1 | 专业开发者通常会遵循规则。据说高级开发者知道何时打破规则,他们能够证明其必要性。另一方面,非常资深开发者通常避免打破规则,以节省时间解释他们的选择。关键是要关注真正影响产品的问题,而不是纠结于细节。 2 | 3 | 编码风格和格式方面,开发者面临许多选择:应该使用制表符还是空格进行缩进?如果是空格,多少个?列或文件的字符限制应该是多少?这些选择通常不会改变程序的行为,但可能会引发无价值的长时间讨论。 4 | 5 | 尽管存在通用做法,但辩论往往围绕个人偏好和轶事证据展开。例如,选择每列80个字符,而不是120个是任意的。重要的是保持一致的样式,不一致可能会妨碍代码的可读性。为确保一致性,建议使用像clang-format这样的格式化工具。这个工具可以通知代码是否格式不正确,甚至可以自动更正。以下是一个格式化代码的示例命令: 6 | 7 | \begin{shell} 8 | clang-format -i --style=LLVM filename1.cpp filename2.cpp 9 | \end{shell} 10 | 11 | 12 | -i 选项指示clang-format直接编辑文件,而 -{}-style 指定要使用的格式化风格,如LLVM、Google、Chromium、Mozilla、WebKit,或提供在文件中的自定义风格(更多细节请参见“扩展阅读”部分)。 13 | 14 | 当然,我们不想每次更改后都手动执行此命令;CMake应该作为构建过程的一部分来处理这个问题。我们已经知道如何在系统上定位clang-format(事先需要手动安装)。尚未了解的是,如何将此外部工具应用于所有源文件。为此,创建一个方便的函数,可以从再cmake目录包含它: 15 | 16 | \filename{ch12/01-formatting/cmake/Format.cmake} 17 | 18 | \begin{cmake} 19 | function(Format target directory) 20 | find_program(CLANG-FORMAT_PATH clang-format REQUIRED) 21 | set(EXPRESSION h hpp hh c cc cxx cpp) 22 | list(TRANSFORM EXPRESSION PREPEND "${directory}/*.") 23 | file(GLOB_RECURSE SOURCE_FILES FOLLOW_SYMLINKS 24 | LIST_DIRECTORIES false ${EXPRESSION} 25 | ) 26 | add_custom_command(TARGET ${target} PRE_BUILD COMMAND 27 | ${CLANG-FORMAT_PATH} -i --style=file ${SOURCE_FILES} 28 | ) 29 | endfunction() 30 | \end{cmake} 31 | 32 | Format函数接受两个参数:target和directory,将在构建目标之前格式化目录中的所有源文件。 33 | 34 | 从技术上讲,目录中的所有文件不必属于目标,且目标源代码可能会分布在多个目录中。然而,尤其是需要排除外部库的头文件时,跟踪与目标相关的所有源文件和头文件会很复杂。这时,关注目录比关注逻辑目标更容易。我们可以为每个需要格式化的目录调用该函数。 35 | 36 | 此函数具有以下步骤: 37 | 38 | \begin{enumerate} 39 | \item 40 | 查找已安装的clang-format二进制文件。如果找不到二进制文件,REQUIRED关键字将使配置停止并报错。 41 | 42 | \item 43 | 创建要格式化的文件扩展名列表(用作globbing表达式)。 44 | 45 | \item 46 | 在每个表达式前加上目录路径。 47 | 48 | \item 49 | 使用之前创建的列表递归搜索源文件和头文件,将找到的文件路径放入SOURCE\_FILES变量(但跳过找到的目录路径)。 50 | 51 | \item 52 | 将格式化命令附加到目标的PRE\_BUILD步骤。 53 | \end{enumerate} 54 | 55 | 这种方法适用于小型到中型代码库。对于大型代码库,可能需要将绝对文件路径转换为相对路径,并使用目录作为工作目录运行格式化命令。这可能是由于shell命令中的字符限制,通常在约13000个字符处达到上限。 56 | 57 | 现在,来探讨如何实际使用这个函数。以下是我们项目的结构: 58 | 59 | \begin{shell} 60 | - CMakeLists.txt 61 | - .clang-format 62 | - cmake 63 | |- Format.cmake 64 | - src 65 | |- CMakeLists.txt 66 | |- header.h 67 | |- main.cpp 68 | \end{shell} 69 | 70 | 首先,设置项目并将cmake目录添加到模块路径中,以便稍后包含: 71 | 72 | \filename{ch12/01-formatting/CMakeLists.txt} 73 | 74 | \begin{cmake} 75 | cmake_minimum_required(VERSION 3.26) 76 | project(Formatting CXX) 77 | enable_testing() 78 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") 79 | add_subdirectory(src bin) 80 | \end{cmake} 81 | 82 | 接下来,填充src目录的列表文件: 83 | 84 | \filename{ch12/01-formatting/src/CMakeLists.txt} 85 | 86 | \begin{cmake} 87 | add_executable(main main.cpp) 88 | include(Format) 89 | Format(main .) 90 | \end{cmake} 91 | 92 | 这很简单。我们创建一个名为main的可执行目标,包含Format.cmake模块,并调用当前目录(src)中main目标的Format()函数。 93 | 94 | 现在,需要一些未格式化的源文件。头文件包含一个简单的未使用函数: 95 | 96 | \filename{ch12/01-formatting/src/header.h} 97 | 98 | \begin{cpp} 99 | int unused() { return 2 + 2; } 100 | \end{cpp} 101 | 102 | 还将包含一个带有过多错误空白的源文件: 103 | 104 | \filename{ch12/01-formatting/src/main.cpp} 105 | 106 | \begin{cpp} 107 | #include <iostream> 108 | using namespace std; 109 | int main() { 110 | cout << "Hello, world!" << endl; 111 | } 112 | \end{cpp} 113 | 114 | 快完成了。只需要格式化器的配置文件,通过 -{}-style=file 命令行参数启用: 115 | 116 | 117 | \filename{ch12/01-formatting/.clang-format} 118 | 119 | \begin{shell} 120 | BasedOnStyle: Google 121 | ColumnLimit: 140 122 | UseTab: Never 123 | AllowShortLoopsOnASingleLine: false 124 | AllowShortFunctionsOnASingleLine: false 125 | AllowShortIfStatementsOnASingleLine: false 126 | \end{shell} 127 | 128 | ClangFormat 将扫描父目录以查找 .clang-format 文件,该文件指定了确切的格式化规则。这让我们可以自定义每一个细节。我这里的情况是,从 Google 的编码风格开始,并做了一些调整:设置 140 个字符的列限制,不使用制表符,并且不允许短循环、函数或单行 if 语句。 129 | 130 | 构建项目后(格式化在编译前自动进行),文件看起来会像这样: 131 | 132 | \filename{ch12/01-formatting/src/header.h (格式化后)} 133 | 134 | \begin{cpp} 135 | int unused() { 136 | return 2 + 2; 137 | } 138 | \end{cpp} 139 | 140 | 即使目标没有使用头文件,其也格式化了。短函数不能放在单行上,正如预期的那样,添加了新行。现在 main.cpp 文件看起来也很整洁。不需要的空白已经消失,缩进也标准化了: 141 | 142 | \filename{ch12/01-formatting/src/main.cpp (formatted)} 143 | 144 | \begin{cpp} 145 | #include <iostream> 146 | using namespace std; 147 | int main() { 148 | cout << "Hello, world!" << endl; 149 | } 150 | \end{cpp} 151 | 152 | 自动化格式化在代码审查期间节省了时间。如果曾经因为空白问题而不得不修改提交,会知道这带来了多大的安慰。一致的格式化让你的代码毫不费力地保持清洁。 153 | 154 | \begin{myNotic}{Note} 155 | 将格式化应用于整个代码库,很可能会对仓库中的大多数文件造成一次性的大更改。如果你(或你的团队成员)正在进行一些工作,这可能会引起很多合并冲突。最好的做法是在所有挂起更改完成后协调这样的努力。如果这不可能,考虑逐步采用,或许可以按目录进行,团队成员会感激你的。 156 | \end{myNotic} 157 | 158 | 尽管格式化器在使代码视觉上一致方面表现出色,但它不是一个全面的程序分析工具。对于更高级的需求,需要设计用于静态分析的其他工具。 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /book/content/chapter12/5.tex: -------------------------------------------------------------------------------- 1 | “你将在阅读代码上花费的时间比编写代码的时间要多,应该优先优化代码的可读性。”这一原则在许多关于整洁代码的书籍中都有所体现。它得到了许多软件开发者的经验支持,这也是为什么像空格数量、换行,以及\#import语句的顺序这样的小细节都要标准化。这种标准化不仅仅是为了做到细致,而是为了节省时间。遵循本章中的实践,可以忘记手动格式化代码。构建时,代码会自动格式化,这是无论如何都要进行的测试代码的步骤。使用ClangFormat,可以确保格式化符合选择的标准。 2 | 3 | 除了简单的空格调整,代码还应该满足许多其他指南。这时clang-tidy就派上用场了,其有助于执行团队或组织约定的编码规范。我们深入讨论了这款静态检查器,还提到了其他选项,如Cpplint、Cppcheck、include-what-you-use和Link What You Use。由于静态链接器相对较快,可以几乎不投入成本地将其添加到构建中,这通常非常值得。 4 | 5 | 我们还检查了Valgrind工具,特别是Memcheck,用于识别内存管理问题,如错误的读写操作。这个工具在避免手动调试数小时,和防止生产环境中出现漏洞方面,具有无法估量的价值。我们介绍了一种方法,通过Memcheck-Cover这个HTML报告生成器,使Valgrind的输出更加用户友好。这在无法运行IDE的环境中使用特别有用,比如CI流程。 6 | 7 | 本章只是一个起点。还有许多其他免费和商业工具有助于提高代码质量。探索它们,找到最适合你的工具。下一章中,我们将深入探讨生成文档的内容。 -------------------------------------------------------------------------------- /book/content/chapter12/6.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | \begin{itemize} 5 | \item 6 | 由C++的作者Bjarne Stroustrup策划的C++核心指南: 7 | 8 | \url{https://github.com/isocpp/CppCoreGuidelines} 9 | 10 | \item 11 | ClangFormat参考手册: 12 | 13 | \url{https://clang.llvm.org/docs/ClangFormat.html} 14 | 15 | \item 16 | C++的静态分析工具——精选列表: 17 | 18 | \url{https://github.com/analysis-tools-dev/static-analysis#cpp} 19 | 20 | \item 21 | CMake内置的静态检查器支持: 22 | 23 | \url{https://www.kitware.com//static-checks-with-cmake-cdash-iwyu-clang-tidy-lwyu-cpplint-and-cppcheck/} 24 | 25 | \item 26 | 启用clang-tidy的目标属性: 27 | 28 | \url{https://cmake.org/cmake/help/latest/prop_tgt/LANG_CLANG_TIDY.html} 29 | 30 | \item 31 | Valgrind手册: 32 | 33 | \url{https://www.valgrind.org/docs/manual/manual-core.html} 34 | \end{itemize} -------------------------------------------------------------------------------- /book/content/chapter12/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter12/images/1.png -------------------------------------------------------------------------------- /book/content/chapter13/0.tex: -------------------------------------------------------------------------------- 1 | 高质量的代码不仅仅是编写良好、可运行且经过测试的——还应该有详尽的文档。文档使我们能够分享可能丢失的信息,描绘更大的蓝图,提供上下文,揭示意图,并且最终——对外部用户和维护者进行介绍。 2 | 3 | 你还记得上次加入一个新项目,在目录和文件的迷宫中迷失数小时的经历吗?这种情况是可以避免的。真正优秀的文档,可以让一个完全的新手在几秒钟内找到他们所需的代码行。遗憾的是,缺少文档的问题常常会被忽视。这并不奇怪——它需要相当多的技巧,而我们中的许多人并不擅长于此。此外,文档和代码很快就会过时。除非实施严格的更新和审查过程,否则很容易忘记文档也需要关注。 4 | 5 | 一些团队(为了节省时间或因为经理鼓励他们这样做)遵循编写自文档化代码的实践,通过为文件名、函数、变量等选择有意义、可读的标识符,希望避免编写文档的麻烦。即使是最优秀的函数签名也不能确保传达所有必要的信息——例如,int removeDuplicates(); 是描述性的,但它没有揭示返回了什么。可能是找到的重复项的数量,剩余项目的数量,或其他东西——都不清楚。虽然良好的命名习惯绝对正确,但它不能取代认真编写文档的行为。记住:世上没有免费的午餐。 6 | 7 | 为了使事情变得更容易,专业人士使用自动文档生成器来分析源文件中的代码和注释,以生成各种格式的全面文档。将这样的生成器添加到 CMake 项目中非常简单——来看看如何操作! 8 | 9 | 本章中,将包含以下内容: 10 | 11 | \begin{itemize} 12 | \item 13 | 将 Doxygen 添加到项目 14 | 15 | \item 16 | 生成具有现代外观的文档 17 | 18 | \item 19 | 使用自定义 HTML 增强输出 20 | \end{itemize} -------------------------------------------------------------------------------- /book/content/chapter13/1.tex: -------------------------------------------------------------------------------- 1 | 可以在GitHub上找到本章中出现的代码文件,地址为 \url{https://github.com/PacktPublishing/Modern-CMake-for-Cpp-2E/tree/main/examples/ch13}。 2 | 3 | 为了构建本书提供的示例,请使用以下推荐命令: 4 | 5 | \begin{shell} 6 | cmake -B <build tree> -S <source tree> 7 | cmake --build <build tree> 8 | \end{shell} 9 | 10 | 请确保将占位符<build tree>和<source tree>替换为适当的路径。提醒一下:<build tree>是目标/输出目录的路径,而<source tree>是源码所在的路径。 11 | 12 | -------------------------------------------------------------------------------- /book/content/chapter13/2.tex: -------------------------------------------------------------------------------- 1 | Doxygen是最成熟和流行的从C++源代码生成文档的工具。当我说“成熟”时,我是认真的:第一个版本是由Dimitri van Heesch在1997年10月发布的。从那时起,它已经广为流传,并且几乎有250名贡献者活跃地支持 (\url{https://github.com/doxygen/doxygen})。 2 | 3 | 可能会担心将Doxygen整合到,没有使用文档生成的较大项目中。确实,为每个函数添加注释可能看起来令人生畏。然而,我鼓励从小处着手。专注于记录最近在最新提交中处理过的元素。记住,即使是部分完整的文档,也比完全没有文档要好,并且它逐渐帮助建立对项目的更全面的理解。 4 | 5 | Doxygen可以生成以下格式的文档: 6 | 7 | \begin{itemize} 8 | \item 9 | 超文本标记语言 (HTML) 10 | 11 | \item 12 | 富文本格式 (RTF) 13 | 14 | \item 15 | 可移植文档格式t (PDF) 16 | 17 | \item 18 | Lamport TeX (LaTeX) 19 | 20 | \item 21 | PostScript (PS) 22 | 23 | \item 24 | Unix 手册 (man 页面) 25 | 26 | \item 27 | 微软HTML帮助手册 (.CHM) 28 | \end{itemize} 29 | 30 | 如果使用Doxygen指定的格式,在代码中添加提供信息注释,其将解析这些注释以丰富输出文件。此外,将分析代码结构以生成有用的图表和图形。后者是可选的,其需要外部Graphviz工具 (\url{https://graphviz.org/})。 31 | 32 | 开发者首先应该考虑以下问题:项目的用户只会接收文档,还是自己生成文档(可能是从源代码构建时)?第一个选项意味着文档随二进制文件分发,在线提供,或者(不太优雅地)与源代码一起检入到仓库中。 33 | 34 | 这很重要,如果希望用户在构建过程中生成文档,将需要在系统中存在依赖项。这并不是一个严重的问题,因为Doxygen和Graphviz可以通过大多数包管理器获得,并且只需要一个简单的命令,例如在Debian上: 35 | 36 | \begin{shell} 37 | apt-get install doxygen graphviz 38 | \end{shell} 39 | 40 | Windows的二进制文件也可以在项目网站上找到(请参阅“扩展阅读”部分)。 41 | 42 | 然而,一些用户可能不太愿意安装这个工具。我们必须决定是为用户生成文档,还是让他们在需要时添加依赖项。项目也可以像第9章描述的那样,为用户自动添加。注意,Doxygen是用CMake构建的。 43 | 44 | 当Doxygen和Graphviz安装到系统中后,可以将生成过程添加到我们的项目中。与一些在线资源建议的相反,这并不像看起来那么困难或复杂。不需要创建外部配置文件,提供Doxygen可执行文件的路径,或添加自定义目标。自从CMake 3.9以来,可以使用FindDoxygen查找模块中的doxygen\_add\_docs()函数,其会设置文档目标。 45 | 46 | \begin{shell} 47 | doxygen_add_docs(targetName [sourceFilesOrDirs...] 48 | [ALL] [WORKING_DIRECTORY dir] [COMMENT comment]) 49 | \end{shell} 50 | 51 | 第一个参数指定目标名称,需要使用-t参数显式构建它,通过cmake生成构建树: 52 | 53 | \begin{shell} 54 | # cmake --build <build-tree> -t targetName 55 | \end{shell} 56 | 57 | 或者,可以通过添加ALL参数来确保始终构建文档。WORKING\_DIRECTORY选项很直接,指定命令的运行目录。COMMENT选项设置的值将在文档生成开始之前显示,提供有用的信息或指令。 58 | 59 | 我们将遵循前几章的做法,并创建一个带有辅助函数的实用模块(以便可以在其他项目中重用: 60 | 61 | \filename{ch13/01-doxygen/cmake/Doxygen.cmake} 62 | 63 | \begin{cmake} 64 | function(Doxygen input output) 65 | find_package(Doxygen) 66 | if (NOT DOXYGEN_FOUND) 67 | add_custom_target(doxygen COMMAND false 68 | COMMENT "Doxygen not found") 69 | return() 70 | endif() 71 | set(DOXYGEN_GENERATE_HTML YES) 72 | set(DOXYGEN_HTML_OUTPUT 73 | ${PROJECT_BINARY_DIR}/${output}) 74 | doxygen_add_docs(doxygen 75 | ${PROJECT_SOURCE_DIR}/${input} 76 | COMMENT "Generate HTML documentation" 77 | ) 78 | endfunction() 79 | \end{cmake} 80 | 81 | 该函数接受两个参数——输入和输出目录——并创建一个自定义的doxygen目标: 82 | 83 | \begin{enumerate} 84 | \item 85 | 首先,使用CMake内置的Doxygen查找模块来确定,系统中是否可用Doxygen。 86 | 87 | \item 88 | 如果不可用,创建一个虚拟的doxygen目标,通知用户并运行false命令(在类Unix系统中返回1,导致构建失败)。我们在此处用return()终止函数。 89 | 90 | \item 91 | 如果Doxygen可用,将其配置为在提供的输出目录中生成HTML输出。Doxygen非常可配置(更多信息请参阅官方文档)。要设置任何选项,只需按照示例调用set(),并在其名称前加上DOXYGEN\_。 92 | 93 | \item 94 | 设置实际的doxygen目标。所有DOXYGEN\_变量都将被转发到Doxygen的配置文件中,并将从源树中提供的输入目录生成文档。 95 | \end{enumerate} 96 | 97 | 如果文档由用户生成,那么第二步可能应该涉及安装Doxygen。 98 | 99 | 要使用这个函数,可以将其纳入我们项目的主列表文件中: 100 | 101 | \filename{ch13/01-doxygen/CMakeLists.txt} 102 | 103 | \begin{cmake} 104 | cmake_minimum_required(VERSION 3.26) 105 | project(Doxygen CXX) 106 | enable_testing() 107 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") 108 | add_subdirectory(src bin) 109 | include(Doxygen) 110 | Doxygen(src docs) 111 | \end{cmake} 112 | 113 | 一点也不难!构建doxygen目标将生成如下所示的HTML文档: 114 | 115 | \myGraphic{0.9}{content/chapter13/images/1.png}{图13.1:使用Doxygen生成的类参考} 116 | 117 | 为了在成员函数文档中添加重要细节,可以在头文件中的C++方法声明前加上适当的注释: 118 | 119 | \filename{ch13/01-doxygen/src/calc.h (片段)} 120 | 121 | \begin{cpp} 122 | /** 123 | Multiply... Who would have thought? 124 | @param a the first factor 125 | @param b the second factor 126 | @result The product 127 | */ 128 | int Multiply(int a, int b); 129 | \end{cpp} 130 | 131 | 这种格式被称为Javadoc。重要的是,注释块要以双星号开始:/**。更多关于Doxygen的docblocks的描述可以在“扩展阅读”部分的链接中找到。带有此类注释的Multiply函数将呈现如下所示的图形: 132 | 133 | \myGraphic{0.5}{content/chapter13/images/2.png}{图13.2:参数和结果的注释} 134 | 135 | 如果安装了Graphviz,Doxygen将检测到它并生成依赖关系图: 136 | 137 | \myGraphic{0.9}{content/chapter13/images/3.png}{图13.3:Doxygen生成的继承和协作图} 138 | 139 | 通过直接从源代码生成文档,建立了一个过程,使得在开发周期中与代码更改同步快速更新成为可能。此外,代码审查期间很可能会注意到忽略的注释更新。 140 | 141 | 许多开发者表示担忧,Doxygen提供的设计看起来过时,这让他们犹豫是否向客户展示生成的文档。然而,这个问题有一个简单的解决方案。 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /book/content/chapter13/3.tex: -------------------------------------------------------------------------------- 1 | 使用清新的设计,来记录项目非常重要。毕竟,如果投入大量工作来为尖端项目编写高质量的文档,那么用户必须将其视为如此。尽管Doxygen功能丰富,但它并不以遵循最新的视觉趋势而闻名。然而,改造其外观并不需要大量的努力。 2 | 3 | 幸运的是,一位名为jothepro的开发者创建了一个名为doxygen-awesome-css的主题,提供了一个现代化、可定制的设计。以下截图展示了这个主题: 4 | 5 | \myGraphic{0.8}{content/chapter13/images/4.png}{图13.4:使用doxygen-awesome-css主题的HTML文档} 6 | 7 | 这个主题不需要任何依赖,可以轻松地从其GitHub页面获取,网址为: \url{https://github.com/jothepro/doxygen-awesome-css}。 8 | 9 | \begin{myNotic}{Note} 10 | 有些在线资源推荐使用应用程序组合,比如通过Sphinx的Breathe和Exhale扩展来转换Doxygen的输出,这种方法可能比较复杂且依赖较多(例如需要Python)。对于一个并非所有成员都深入了解CMake的团队来说,更简单的方法通常更实用。 11 | \end{myNotic} 12 | 13 | 我们可以通过一个自动化过程高效地实现这个主题。看看如何通过添加一个新的宏,来扩展Doxygen.cmake文件并使用: 14 | 15 | \filename{ch13/02-doxygen-nice/cmake/Doxygen.cmake (片段)} 16 | 17 | \begin{cmake} 18 | macro(UseDoxygenAwesomeCss) 19 | include(FetchContent) 20 | FetchContent_Declare(doxygen-awesome-css 21 | GIT_REPOSITORY 22 | https://github.com/jothepro/doxygen-awesome-css.git 23 | GIT_TAG 24 | V2.3.1 25 | ) 26 | FetchContent_MakeAvailable(doxygen-awesome-css) 27 | set(DOXYGEN_GENERATE_TREEVIEW YES) 28 | set(DOXYGEN_HAVE_DOT YES) 29 | set(DOXYGEN_DOT_IMAGE_FORMAT svg) 30 | set(DOXYGEN_DOT_TRANSPARENT YES) 31 | set(DOXYGEN_HTML_EXTRA_STYLESHEET 32 | ${doxygen-awesome-css_SOURCE_DIR}/doxygen-awesome.css) 33 | endmacro() 34 | \end{cmake} 35 | 36 | 我们已经从本书的前几章中了解了所有这些命令,但为了完全清晰,再来看看发生了什么: 37 | 38 | \begin{enumerate} 39 | \item 40 | 使用FetchContent模块从Git获取doxygen-awesome-css 41 | 42 | \item 43 | 为Doxygen配置额外的选项(这些选项特别由主题的README文件推荐) 44 | 45 | \item 46 | 将主题的css文件复制到Doxygen的输出目录 47 | \end{enumerate} 48 | 49 | 最好在Doxygen函数中调用这个宏,正好在doxygen\_add\_docs()之前: 50 | 51 | \filename{ch13/02-doxygen-nice/cmake/Doxygen.cmake (片段)} 52 | 53 | \begin{cmake} 54 | function(Doxygen input output) 55 | # ... 56 | UseDoxygenAwesomeCss() 57 | doxygen_add_docs (...) 58 | endfunction() 59 | 60 | macro(UseDoxygenAwesomeCss) 61 | # ... 62 | endmacro() 63 | \end{cmake} 64 | 65 | 记住,宏中的所有变量都在调用函数的作用域内设置。现在,可以享受生成的HTML文档中的现代风格,并且自豪地与世界分享。然而,我们的演示主题提供了一些JavaScript模块来增强体验。 66 | 67 | 最后,我们要如何包含它们? 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /book/content/chapter13/4.tex: -------------------------------------------------------------------------------- 1 | Doxygen Awesome 提供了一些额外的功能,这些功能可以通过在文档头部的 HTML <head> 标签内包含几个 JavaScript 代码片段来启用。这些功能非常有用,允许用户在浅色和深色模式之间切换、为代码片段添加复制按钮、提供段落标题的永久链接以及创建交互式的目录。 2 | 3 | 然而,实现这些功能需要将额外的代码复制到输出目录,并将其包含在生成的 HTML 文件中。 4 | 5 | 以下是需要在 </head> 标签前包含的 JavaScript 代码: 6 | 7 | \filename{ch13/cmake/extra\_headers} 8 | 9 | \begin{minted}[frame=single]{html} 10 | <script type="text/javascript" src="$relpath^doxygen-awesome-darkmodetoggle.js"></script> 11 | <script type="text/javascript" src="$relpath^doxygen-awesome-fragmentcopy-button.js"></script> 12 | <script type="text/javascript" src="$relpath^doxygen-awesome-paragraphlink.js"></script> 13 | <script type="text/javascript" src="$relpath^doxygen-awesome-interactivetoc.js"></script> 14 | 15 | <script type="text/javascript"> 16 | DoxygenAwesomeDarkModeToggle.init() 17 | DoxygenAwesomeFragmentCopyButton.init() 18 | DoxygenAwesomeParagraphLink.init() 19 | DoxygenAwesomeInteractiveToc.init() 20 | </script> 21 | \end{minted} 22 | 23 | 这段代码首先会包含几个 JavaScript 文件,然后初始化不同的扩展。不幸的是,这段代码不能简单地添加到某个变量中。相反,需要用一个自定义文件覆盖默认的头部文件。这种覆盖可以通过向 Doxygen 的 HTML\_HEADER 配置变量提供该文件的路径来完成。 24 | 25 | 为了创建一个自定义头部而无需硬编码全部内容,可以使用 Doxygen 的命令行工具来生成一个默认的头部文件,并在生成文档之前编辑它: 26 | 27 | \begin{shell} 28 | doxygen -w html header.html footer.html style.css 29 | \end{shell} 30 | 31 | 尽管我们不会使用或更改 footer.html 或 style.css,但它们是必需的参数,所以无论如何都需要创建它们。 32 | 33 | 最后,需要自动在 </head> 标签前插入 ch13/cmake/extra\_headers 文件的内容以包含所需的 JavaScript。这可以通过 Unix 命令行工具 sed 来完成,其会就地编辑 header.html 文件: 34 | 35 | \begin{shell} 36 | sed -i '/<\/head>/r ch13/cmake/extra_headers' header.html 37 | \end{shell} 38 | 39 | 现在需要用 CMake 语言来编写这些步骤。以下是实现这一目标的宏: 40 | 41 | \filename{ch13/02-doxygen-nice/cmake/Doxygen.cmake (片段)} 42 | 43 | \begin{cmake} 44 | macro(UseDoxygenAwesomeExtensions) 45 | set(DOXYGEN_HTML_EXTRA_FILES 46 | ${doxygen-awesome-css_SOURCE_DIR}/doxygen-awesome-darkmode-toggle.js 47 | ${doxygen-awesome-css_SOURCE_DIR}/doxygen-awesome-fragment-copybutton.js 48 | ${doxygen-awesome-css_SOURCE_DIR}/doxygen-awesome-paragraph-link.js 49 | ${doxygen-awesome-css_SOURCE_DIR}/doxygen-awesome-interactive-toc.js 50 | ) 51 | 52 | execute_process( 53 | COMMAND doxygen -w html header.html footer.html style.css 54 | WORKING_DIRECTORY ${PROJECT_BINARY_DIR} 55 | ) 56 | execute_process( 57 | COMMAND sed -i 58 | "/<\\/head>/r ${PROJECT_SOURCE_DIR}/cmake/extra_headers" 59 | header.html 60 | WORKING_DIRECTORY ${PROJECT_BINARY_DIR} 61 | ) 62 | set(DOXYGEN_HTML_HEADER ${PROJECT_BINARY_DIR}/header.html) 63 | endmacro() 64 | \end{cmake} 65 | 66 | 这段代码看起来很复杂,但仔细看过后,会发现它其实相当简单。 67 | 68 | 下面列出它的功能: 69 | 70 | \begin{enumerate} 71 | \item 72 | 将四个 JavaScript 文件复制到输出目录 73 | 74 | \item 75 | 执行 doxygen 命令来生成默认的 HTML 文件 76 | 77 | \item 78 | 执行 sed 命令将所需的 JavaScript 注入头部 79 | 80 | \item 81 | 用自定义版本覆盖默认的头部 82 | \end{enumerate} 83 | 84 | 为了完成集成,可以在启用基本样式表之后调用这个宏: 85 | 86 | \filename{ch13/02-doxygen-nice/cmake/Doxygen.cmake (fragment)} 87 | 88 | \begin{cmake} 89 | function(Doxygen input output) 90 | # … 91 | UseDoxygenAwesomeCss() 92 | UseDoxygenAwesomeExtensions() 93 | # … 94 | endfunction() 95 | \end{cmake} 96 | 97 | 本例的完整代码以及实际示例可以在本书的在线仓库中找到,我建议你在实际环境中阅读和探索这些示例。 98 | 99 | \begin{myNotic}{其他文档生成工具} 100 | 有许多其他工具没有在这本书中涵盖,我们主要关注由 CMake 支持的项目。不过,其中一些可能更适合各位的具体情况。如果愿意尝试新事物,可以访问两个我觉得有趣的项目的网站: 101 | 102 | \begin{itemize} 103 | \item 104 | Adobe 的 Hyde (\url{https://github.com/adobe/hyde}):针对 Clang 编译器设计,Hyde 生成 Markdown 文件,这些文件可以像 Jekyll(https://jekyllrb.com/)这样的静态页面生成器消费,Jekyll 是 GitHub 支持的工具 105 | 106 | \item 107 | Standardese (\url{https://github.com/standardese/standardese}):使用 libclang 编译代码,并提供 HTML、Markdown、LaTeX 和手册页格式的输出,有望成为下一个 Doxygen。 108 | \end{itemize} 109 | \end{myNotic} 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /book/content/chapter13/5.tex: -------------------------------------------------------------------------------- 1 | 本章中,深入探讨了将Doxygen这一强大的文档生成工具添加到您的CMake项目中,并提升其吸引力的实际操作。尽管这项任务看起来有些令人生畏,但实际上它是相当易于管理的,并且能显著提升解决方案中信息的流动性和清晰度。各位到后面会发现,特别是当个人或团队成员努力理解应用程序中的复杂关系时,投入时间和精力添加和维护文档是一笔值得的投资。探讨了如何使用CMake内置的Doxygen支持来实际生成文档之后,我们稍微转变了一下方向,确保文档不仅可读,而且易读。 2 | 3 | 由于过时的设计可能对眼睛造成负担,我们探讨了生成HTML的替代外观。这是通过使用Doxygen Awesome扩展来完成的。为了启用它带来的增强功能,我们通过添加必要的javascript来自定义标准页头。 4 | 5 | 通过生成文档,确保了它与实际代码的接近性,这使得将书面解释与逻辑保持同步变得更加容易,尤其是它们都在同一个文件中。此外,作为开发者,可能同时在处理许多任务和细节。文档作为一种记忆辅助工具,帮助保留和回忆项目的复杂性。记住,“即使是最短的铅笔也比最长的记忆要长。”帮助一下自己——把写下来,从而取得成功。 6 | 7 | 总结来说,本章强调了Doxygen在项目管理工具中的价值,有助于团队的理解和沟通。 8 | 9 | 下一章中,我将介绍如何使用CMake自动化项目的打包和安装,进一步提升项目的管理技巧。 -------------------------------------------------------------------------------- /book/content/chapter13/6.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | \begin{itemize} 4 | \item 5 | Doxygen 官方网站: 6 | 7 | \url{https://www.doxygen.nl/} 8 | 9 | \item 10 | FindDoxygen 查找模块文档: 11 | 12 | \url{https://cmake.org/cmake/help/latest/module/FindDoxygen.html} 13 | 14 | \item 15 | Doxygen 的文档: 16 | 17 | \url{https://www.doxygen.nl/manual/docblocks.html#specialblock} 18 | \end{itemize} -------------------------------------------------------------------------------- /book/content/chapter13/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter13/images/1.png -------------------------------------------------------------------------------- /book/content/chapter13/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter13/images/2.png -------------------------------------------------------------------------------- /book/content/chapter13/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter13/images/3.png -------------------------------------------------------------------------------- /book/content/chapter13/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter13/images/4.png -------------------------------------------------------------------------------- /book/content/chapter14/0.tex: -------------------------------------------------------------------------------- 1 | 我们的项目已经构建、测试并记录在案。现在,到了将其发布给用户的时候。本章主要关注需要采取的最后两个步骤:安装和打包。这些是我们迄今为止所学的一切基础上的高级技术:管理目标和它们的依赖关系,短暂的使用需求,生成器表达式等。 2 | 3 | 安装使得项目能够在整个系统中发现和访问。我们将介绍如何在不进行安装的情况下导出目标,以供其他项目使用;以及如何安装项目,以便于整个系统轻松访问。将了解如何配置项目,以自动将各种工件类型放置到适当的目录中。为了处理更高级的场景,将介绍用于安装文件和目录,以及执行自定义脚本和 CMake 命令的低层命令。 4 | 5 | 接下来,将探索设置可重用的 CMake 包,其他项目可以使用 find\_package() 命令来发现它们。我们将解释如何确保目标,和定义特定文件的系统位置。我们还将讨论如何编写基本和高级的配置文件,以及与包相关联的版本文件。然后,为了模块化,我们将简要介绍组件的概念,这既适用于 CMake 包也适用于 install() 命令。所有这些准备工作将为本章最后要介绍的方面铺平道路:使用 CPack 生成各种操作系统中的包管理器都能识别的存档、安装程序、捆绑包和包。这些包可以分发预构建的工件、可执行文件和库。这是最终用户开始使用软件的最简单方式。 6 | 7 | 本章中,将包含以下内容: 8 | 9 | \begin{itemize} 10 | \item 11 | 无需安装即可导出 12 | 13 | \item 14 | 在系统上安装项目 15 | 16 | \item 17 | 创建可重用包 18 | 19 | \item 20 | 定义组件 21 | 22 | \item 23 | 使用 CPack 打包 24 | \end{itemize} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /book/content/chapter14/1.tex: -------------------------------------------------------------------------------- 1 | 可以在GitHub上的\url{https://github.com/PacktPublishing/Modern-CMake-for-Cpp-2E/tree/main/examples/ch14}找到本章中出现的代码文件。 2 | 3 | 要构建本书提供的示例,请使用以下推荐命令: 4 | 5 | \begin{shell} 6 | cmake -B <build tree> -S <source tree> 7 | cmake --build <build tree> 8 | \end{shell} 9 | 10 | 要安装示例,请使用以下命令: 11 | 12 | \begin{shell} 13 | cmake --install <build tree> 14 | \end{shell} 15 | 16 | 请确保将<build tree>和<source tree>占位符替换为适当的路径。提醒一下:<build tree>是目标/输出目录的路径,而<source tree>是源码所在的路径。 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /book/content/chapter14/2.tex: -------------------------------------------------------------------------------- 1 | 我们如何使项目A的目标对项目B可用?通常,我们会使用find\_package()命令,但这需要创建一个包并在系统上安装。虽然这种方法有用,但是略显麻烦。有时,我们构建一个项目后,希望有一种简单的方法使得项目中的目标可以被其他项目使用。 2 | 3 | 一种简单的方法是直接在项目B中包含项目A的主CMakeLists.txt,该文件中包含了所有目标定义。然而,这个文件还会包括全局配置、具有副作用的CMake命令、额外的依赖项,以及B项目可能不需要的目标(比如单元测试)。因此,这种方法并不太好。我们可以使用如下的方式,在项目A中导出一个.cmake文件(称为目标导出文件),然后在项目B中通过include()命令包含该文件,实现该目的: 4 | 5 | \begin{cmake} 6 | cmake_minimum_required(VERSION 3.26.0) 7 | project(B) 8 | include(/path/to/A/TargetsOfA.cmake) 9 | \end{cmake} 10 | 11 | 项目A的目标导出文件中包含使用add\_library()和add\_executable()等命令定义的所有目标,并包含为这些目标设置的属性。 12 | 13 | 项目A中使用如下命令生成目标导出文件,命令中必须在TARGETS关键字后指定要导出的所有目标,并在FILE后提供目标文件名。其他参数是可选的: 14 | 15 | \begin{shell} 16 | export(TARGETS [target1 [target2 [...]]] 17 | [NAMESPACE <namespace>] [APPEND] FILE <path> 18 | [EXPORT_LINK_INTERFACE_LIBRARIES] 19 | ) 20 | \end{shell} 21 | 22 | 其中的参数: 23 | 24 | \begin{itemize} 25 | \item 26 | NAMESPACE: 目标导出文件中,所有目标前都会包含NAMESPACE指定的前缀,这些目标被导入其他项目使用时,也应该包含该前缀。推荐为导出的目标都指定前缀,以在使用时可以知道目标是从其他项目导入的。 27 | 28 | \item 29 | APPEND: 将导出的内容追加到 FILE 指定的文件,避免写入前清除原文件的内容。 30 | 31 | \item 32 | EXPORT\_LINK\_INTERFACE\_LIBRARIES: 同时导出目标的链接依赖项(包括导入的和特定配置的变体)。 33 | \end{itemize} 34 | 35 | 让我们将这种导出方法应用到Calc库示例中,该库提供了两个简单的方法: 36 | 37 | \filename{ch14/01-export/src/include/calc/basic.h} 38 | 39 | \begin{cpp} 40 | #pragma once 41 | int Sum(int a, int b); 42 | int Multiply(int a, int b); 43 | \end{cpp} 44 | 45 | 首先需要声明Calc目标以便我们有东西可以导出: 46 | 47 | \filename{ch14/01-export/src/CMakeLists.txt} 48 | 49 | \begin{cmake} 50 | add_library(calc STATIC basic.cpp) 51 | target_include_directories(calc INTERFACE include) 52 | \end{cmake} 53 | 54 | 然后,使用export(TARGETS)命令生成导出文件: 55 | 56 | \filename{ch14/01-export/CMakeLists.txt (fragment)} 57 | 58 | \begin{cmake} 59 | cmake_minimum_required(VERSION 3.26) 60 | project(ExportCalc CXX) 61 | add_subdirectory(src bin) 62 | set(EXPORT_DIR "${CMAKE_CURRENT_BINARY_DIR}/cmake") 63 | export(TARGETS calc 64 | FILE "${EXPORT_DIR}/CalcTargets.cmake" 65 | NAMESPACE Calc:: 66 | ) 67 | \end{cmake} 68 | 69 | 如果想要将目标声明文件导到构建树的cmake子目录中(遵循.cmake文件的约定),为了能够复用路径,将出目录路径设置在EXPORT\_DIR变量中。然后,调用export()来生成目标声明文件CalcTargets.cmake,其中包含了定义的calc目标。对于使用include包含这个文件的项目,可以直接通过Calc::calc使用目标。 70 | 71 | 注意,这个导出文件还不是一个包。更重要的是,这个文件中的所有路径都是绝对路径,并且硬编码到构建树中,这使得它们不可重定位。 72 | 73 | export()命令还有一个使用EXPORT关键字的简写版本: 74 | 75 | \begin{shell} 76 | export(EXPORT <export> [NAMESPACE <namespace>] [FILE <path>]) 77 | \end{shell} 78 | 79 | 然而,它需要一个预定义的导出名称,而不是要导出的目标列表, <export>这样的实例是由install(TARGETS)创建的目标命名列表。 80 | 81 | 以下是如何在实践中使用这种简写的示例: 82 | 83 | \filename{ch14/01-export/CMakeLists.txt (continued)} 84 | 85 | \begin{cmake} 86 | install(TARGETS calc EXPORT CalcTargets) 87 | export(EXPORT CalcTargets 88 | FILE "${EXPORT_DIR}/CalcTargets2.cmake" 89 | NAMESPACE Calc:: 90 | ) 91 | \end{cmake} 92 | 93 | 这段代码的工作方式与前面的示例类似,但现在它在export()和install()命令之间共享了同一个目标列表。 94 | 95 | 两种生成导出文件的方法产生类似的结果,包括一些样板代码和定义目标的几行。将<build-tree>设置为构建树路径后,将创建一个类似以下的目标导出文件: 96 | 97 | \filename{<build-tree>/cmake/CalcTargets.cmake (片段)} 98 | 99 | \begin{cmake} 100 | # Create imported target Calc::calc 101 | add_library(Calc::calc STATIC IMPORTED) 102 | set_target_properties(Calc::calc PROPERTIES 103 | INTERFACE_INCLUDE_DIRECTORIES 104 | "/<source-tree>/include" 105 | ) 106 | # Import target "Calc::calc" for configuration "" 107 | set_property(TARGET Calc::calc APPEND PROPERTY 108 | IMPORTED_CONFIGURATIONS NOCONFIG 109 | ) 110 | set_target_properties(Calc::calc PROPERTIES 111 | IMPORTED_LINK_INTERFACE_LANGUAGES_NOCONFIG "CXX" 112 | IMPORTED_LOCATION_NOCONFIG "/<build-tree>/libcalc.a" 113 | ) 114 | \end{cmake} 115 | 116 | 通常,我们不会编辑甚至打开这个文件,但重要的是文件中的路径为硬编码的(参见突出显示的行)。在当前的形式下,构建的项目不可重定位。要改变这一点,需要采取一些其他步骤。下一节中,我们将解释什么是重定位,及其重要性。 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /book/content/chapter14/5.tex: -------------------------------------------------------------------------------- 1 | 2 | 我们将从解释术语“组件”可能引起的混淆开始。考虑find\_package()的完整签名: 3 | 4 | \begin{shell} 5 | find_package(<PackageName> 6 | [version] [EXACT] [QUIET] [MODULE] [REQUIRED] 7 | [[COMPONENTS] [components...]] 8 | [OPTIONAL_COMPONENTS components...] 9 | [NO_POLICY_SCOPE] 10 | ) 11 | \end{shell} 12 | 13 | 重要的是不要将这里提到的组件,与install()命令中使用的COMPONENT关键字混淆。尽管它们名称相同,但是不同的概念,必须分开理解。我们将在以下子节中进一步探讨这一点。 14 | 15 | \mySubsubsection{14.5.1.}{如何在find\_package()中使用组件} 16 | 17 | 当调用带有COMPONENTS或OPTIONAL\_COMPONENTS列表的find\_package()时,告诉CMake我们只对提供这些组件的包感兴趣。然而,理解验证这一要求是包的责任至关重要。如果包提供商没有在配置文件中实现必要的检查,则不会按预期进行。 18 | 19 | 请求的组件通过<package>\_FIND\_COMPONENTS变量传递给配置文件(包括可选和不可选的)。对于每个不可选组件,都会设置一个<package>\_FIND\_REQUIRED\_<component>变量。包作者可以编写宏来扫描这个列表,并验证所有必需组件的提供情况。check\_required\_components()函数就为此目的服务。当找到必要的组件时,配置文件应设置<package>\_<component>\_FOUND变量。文件末尾的一个宏将验证是否设置了所有必需的变量。 20 | 21 | \mySubsubsection{14.5.2.}{如何在install()命令中使用组件} 22 | 23 | 并非在所有情况下都需要安装所有生成的工件。例如,一个项目可能为了开发而安装静态库和公共头文件,但默认情况下,可能只需要为运行时安装一个共享库。为了启用这种双重行为,可以使用COMPONENT关键字将工件分组在install()命令下的一个通用名称下。有兴趣限制安装到特定组件的用户可以通过执行以下区分大小写的命令来实现: 24 | 25 | \begin{shell} 26 | cmake --install <build tree> 27 | --component=<component1 name> --component=<component2 name> 28 | \end{shell} 29 | 30 | 为工件分配COMPONENT关键字并不会自动将其从默认安装中排除。要实现这种排除,必须添加EXCLUDE\_FROM\_ALL关键字。 31 | 32 | 让我们在代码示例中探讨这个概念: 33 | 34 | \filename{ch14/13-components/CMakeLists.txt (片段)} 35 | 36 | \begin{cmake} 37 | install(TARGETS calc EXPORT CalcTargets 38 | ARCHIVE 39 | COMPONENT lib 40 | FILE_SET HEADERS 41 | COMPONENT headers 42 | ) 43 | install(EXPORT CalcTargets 44 | DESTINATION ${CMAKE_INSTALL_LIBDIR}/calc/cmake 45 | NAMESPACE Calc:: 46 | COMPONENT lib 47 | ) 48 | install(CODE "MESSAGE(\"Installing 'extra' component\")" 49 | COMPONENT extra 50 | EXCLUDE_FROM_ALL 51 | ) 52 | \end{cmake} 53 | 54 | 前面的安装命令定义了以下组件: 55 | 56 | \begin{itemize} 57 | \item 58 | lib: 这包含静态库和目标导出文件。默认安装。 59 | 60 | \item 61 | headers: 这包含C++头文件。也默认安装。 62 | 63 | \item 64 | extra: 这执行一段代码以打印消息。不默认安装。 65 | \end{itemize} 66 | 67 | 让我们重申: 68 | 69 | \begin{itemize} 70 | \item 71 | cmake -{}-install 没有 -{}-component 参数将安装lib和headers组件。 72 | 73 | \item 74 | cmake -{}-install -{}-component headers 将只安装公共头文件。 75 | 76 | \item 77 | cmake -{}-install -{}-component extra 将打印一条消息,其他命令不会打印(EXCLUDE\_FROM\_ALL关键字阻止了这一点)。 78 | \end{itemize} 79 | 80 | 如果没有为安装的工件指定COMPONENT关键字,它默认为未指定,由CMAKE\_INSTALL\_DEFAULT\_COMPONENT\_NAME变量定义。 81 | 82 | \begin{myNotic}{Note} 83 | 由于无法从cmake命令行列出所有可用组件,因此记录包的所有组件对于用户来说可能非常有帮助。安装"README"文件是放置这些信息的绝佳位置。 84 | \end{myNotic} 85 | 86 | 如果cmake使用-{}-component参数调用一个不存在的组件,命令将成功完成,不会有警告或错误,但不会安装任何东西。 87 | 88 | 将我们的安装划分为组件,使得用户可以选择性地安装包的某一部分。现在来管理版本化共享库的符号链接,这是一个优化你的安装过程的有用功能。 89 | 90 | \mySubsubsection{14.5.3.}{管理版本化共享库的符号链接} 91 | 92 | 有些安装的目标平台可以使用符号链接,来帮助链接器发现共享库的当前安装版本。如通常创建 lib<name>.so 符号链接链接到 lib<name>.so.1 实际库文件(可以使用set\_property()指令给动态库目标指定版本,这时该目标包含有数字后缀的实际库文件和无数字后缀的链接到实际库文件的符号链接),之后就可以通过向链接器统一传递 -l<name> 参数(不需要指定共享库的版本后缀)来链接这样的库。 93 | 94 | CMake 的 install(TARGETS <target> LIBRARY) 在安装时会同时安装符号链接和实际库文件。也可以只安装实际库文件,将符号链接的安装移到另一个 install() 命令中。这通过在这个块中添加 NAMELINK\_SKIP 来实现: 95 | 96 | \begin{shell} 97 | install(TARGETS <target> LIBRARY 98 | COMPONENT cmp NAMELINK_SKIP) 99 | \end{shell} 100 | 101 | 以上指令在执行安装时,只会安装 <target> 中的实际库文件,且 cmp 组件中只包含实际库文件。 102 | 103 | 可以使用有 NAMELINK\_ONLY 关键词的 install() 命令只安装符号链接文件: 104 | 105 | \begin{shell} 106 | install(TARGETS <target> LIBRARY 107 | COMPONENT lnk NAMELINK_ONLY) 108 | \end{shell} 109 | 110 | 以上指令在执行安装时,只会安装 <target> 中的符号链接,且 lnk 组件中只包含符号链接。 111 | 112 | 以上的效果也可以通过使用 NAMELINK\_COMPONENT 关键词来达到。以下命令执行后,会同时安装 <target> 中的实际库文件和符号链接,但是 cmp 组件中只包含实际库文件, lnk 组件中只包含符号链接: 113 | 114 | \begin{shell} 115 | install(TARGETS <target> LIBRARY 116 | COMPONENT cmp NAMELINK_COMPONENT lnk) 117 | \end{shell} 118 | 119 | 现在我们已经配置了自动安装过程,可以使用随 CMake 一起提供的 CPack 工具来为用户提供预构建的工件。 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /book/content/chapter14/6.tex: -------------------------------------------------------------------------------- 1 | 虽然从源代码构建项目有其好处,但对于最终用户特别是非开发人员来说,这可能会既耗时又复杂。一种更方便的分发方法是使用二进制包,其中包含了编译后的制品和其他必要的静态文件。CMake 支持使用名为 cpack 的命令行工具来生成此类包。 2 | 3 | 要生成一个包,需要为目标平台和包类型选择一个合适的包生成器。不要将包生成器与像 Unix Makefiles 或 Visual Studio 这样的构建系统生成器混淆。 4 | 5 | 下表列出了可用的包生成器: 6 | 7 | % Please add the following required packages to your document preamble: 8 | % \usepackage{longtable} 9 | % Note: It may be necessary to compile the document several times to get a multi-page table to line up properly 10 | \begin{longtable}{|l|l|l|} 11 | \hline 12 | \textbf{生成器名称} & \textbf{生成的文件类型} & \textbf{平台} \\ \hline 13 | \endfirsthead 14 | % 15 | \endhead 16 | % 17 | Archive & 18 | \begin{tabular}[c]{@{}l@{}}7Z, 7zip - (.7z)\\ TBZ2 (.tar.bz2)\\ TGZ (.tar.gz)\\ TXZ (.tar.xz)\\ TZ (.tar.Z)\\ TZST (.tar.zst)\\ ZIP (.zip)\end{tabular} & 19 | 跨平台 \\ \hline 20 | Bundle & macOs Bundle (.bundle) & macOS \\ \hline 21 | Cygwin & Cygwin packages & Cygwin \\ \hline 22 | DEB & Debian packages (.deb) & Linux \\ \hline 23 | External & 第三方打包程序使用的 JSON (.json) 文件 & 跨平台 \\ \hline 24 | FreeBSD & PKG (.pkg) & *BSD, Linux, macOS \\ \hline 25 | IFW & QT 安装程序二进制文件 & Linux, Windows, macOS \\ \hline 26 | NSIS & Binary (.exe) & Windows \\ \hline 27 | NuGet & NuGet 包 (.nupkg) & Windows \\ \hline 28 | productbuild & PKG (.pkg) & macOS \\ \hline 29 | RPM & RPM (.rpm) & Linux \\ \hline 30 | WIX & Microsoft Installer (.msi) & Windows \\ \hline 31 | \end{longtable} 32 | 33 | \begin{center} 34 | 表 14.3: 可用的包生成器 35 | \end{center} 36 | 37 | 大多数这些生成器都有广泛的配置选项。本书不打算深入探讨所有细节,相应信息可以在“扩展阅读”部分找到更多信息。 38 | 39 | 为了使用 CPack,需要使用必要的 install() 命令来配置项目的安装,并构建项目。CPack 会根据构建树中的 CPackConfig.cmake 文件准备二进制包。虽然可以手动创建这个文件,但在项目的列表文件中使用 include(CPack) 更加简便,它会在构建树中生成配置文件并提供所需的默认值。 40 | 41 | 让我们扩展 13-components 示例以供 CPack 使用: 42 | 43 | \filename{ch14/14-cpack/CMakeLists.txt (fragment)} 44 | 45 | \begin{cmake} 46 | cmake_minimum_required(VERSION 3.26) 47 | project(CPackPackage VERSION 1.2.3 LANGUAGES CXX) 48 | include(GNUInstallDirs) 49 | add_subdirectory(src bin) 50 | install(...) 51 | install(...) 52 | install(...) 53 | set(CPACK_PACKAGE_VENDOR "Rafal Swidzinski") 54 | set(CPACK_PACKAGE_CONTACT "email@example.com") 55 | set(CPACK_PACKAGE_DESCRIPTION "Simple Calculator") 56 | include(CPack) 57 | \end{cmake} 58 | 59 | CPack 模块从 project() 命令中提取以下变量: 60 | 61 | \begin{itemize} 62 | \item 63 | CPACK\_PACKAGE\_NAME 64 | 65 | \item 66 | CPACK\_PACKAGE\_VERSION 67 | 68 | \item 69 | CPACK\_PACKAGE\_FILE\_NAME 70 | \end{itemize} 71 | 72 | The CPACK\_PACKAGE\_FILE\_NAME 存储了包名的结构: 73 | 74 | \begin{shell} 75 | $CPACK_PACKAGE_NAME-$CPACK_PACKAGE_VERSION-$CPACK_SYSTEM_NAME 76 | \end{shell} 77 | 78 | CPACK\_SYSTEM\_NAME 是目标操作系统的名称,例如 Linux 或 win32。例如,在 Debian 上执行 ZIP 生成器时,CPack 将生成一个名为 CPackPackage-1.2.3-Linux.zip 的文件。 79 | 80 | 要在构建项目后生成包,请转到项目的构建树并运行: 81 | 82 | \begin{shell} 83 | cpack [<options>] 84 | \end{shell} 85 | 86 | CPack 从 CPackConfig.cmake 文件读取选项,可以覆盖这些设置: 87 | 88 | \begin{itemize} 89 | \item 90 | -G <generators>: 以分号分隔的包生成器列表。默认值可以在 CPackConfig.cmake 中的 CPACK\_GENERATOR 变量中指定。 91 | 92 | \item 93 | -C <configs>: 以分号分隔的构建配置列表(debug, release),用于生成包(对于多配置构建系统生成器是必需的)。 94 | 95 | \item 96 | -D <var>=<value>: 此选项覆盖 CPackConfig.cmake 文件中设置的变量。 97 | 98 | \item 99 | -{}-config <config-file>: 此选项使用指定的配置文件代替默认的 CPackConfig.cmake 文件。 100 | cmake. 101 | 102 | \item 103 | -{}-verbose, -V: 此选项提供详细的输出。 104 | 105 | \item 106 | -P <packageName>: 此选项覆盖包名。 107 | 108 | \item 109 | -R <packageVersion>: 此选项覆盖包版本。 110 | 111 | \item 112 | -{}-vendor <vendorName>: 此选项覆盖包供应商。 113 | 114 | \item 115 | -B <packageDirectory>: 此选项指定 cpack 的输出目录(默认情况下,这将是当前工作目录)。 116 | \end{itemize} 117 | 118 | 让我们尝试为我们的 14-cpack 示例项目生成包。我们将使用 ZIP、7Z 和 Debian 包生成器: 119 | 120 | \begin{shell} 121 | cpack -G "ZIP;7Z;DEB" -B packages 122 | \end{shell} 123 | 124 | 应该会得到以下这些包: 125 | 126 | \begin{itemize} 127 | \item 128 | CPackPackage-1.2.3-Linux.7z 129 | 130 | \item 131 | CPackPackage-1.2.3-Linux.deb 132 | 133 | \item 134 | CPackPackage-1.2.3-Linux.zip 135 | \end{itemize} 136 | 137 | 这些二进制包已准备好发布在项目网站上、GitHub Release页面中,或作为最终用户的包存储库。 138 | -------------------------------------------------------------------------------- /book/content/chapter14/7.tex: -------------------------------------------------------------------------------- 1 | 编写跨平台安装脚本的复杂性可能会令人望而却步,但 CMake 显著简化了这一任务。尽管它需要一些初始设置,CMake 流程化了这一过程,与这本书中探讨的概念和技术无缝集成。 2 | 3 | 我们从理解如何从项目中导出 CMake 目标开始,使得它们可以在不需要安装的情况下在其他项目中使用。接下来,深入了解已经为导出配置的项目的安装。探讨安装基础时,专注于一个关键方面:安装 CMake 目标。现在掌握了 CMake 如何为不同工件类型分配不同目的地,以及公共头文件的特殊考虑。我们还检查了 install() 命令的其他模式,包括安装文件、程序和目录,以及在安装过程中执行脚本。 4 | 5 | 然后,CMake可创建重用包。我们探索了如何使项目目标可重定位,从而方便用户定义安装位置。这包括创建可以通过 find\_package() 使用的完全定义的包,涉及准备目标导出文件、配置文件和版本文件。考虑到不同用户的需求,了解了如何将工件和操作分组到安装组件中,将它们与 CMake 包的组件区分开来。我们的探索最终以 CPack 的介绍达到尾声,我们了解了如何准备基本的二进制包,提供了一种有效的方法来分发预编译的软件。虽然掌握 CMake 中安装和打包的细微之处是一个持续的过程,但这一章为我们奠定了坚实的基础。它使我们能够处理常见场景,并且自信地进一步深入。 6 | 7 | 下一章中,我们将运用我们积累的知识,通过创建一个连贯的、专业级别的项目,展示这些 CMake 技术的实际应用。 -------------------------------------------------------------------------------- /book/content/chapter14/8.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | \begin{itemize} 4 | \item 5 | GNU目标编码标准 6 | 7 | \url{https://www.gnu.org/prep/standards/html_node/Directory-Variables.html} 8 | 9 | \item 10 | 讨论使用FILE\_SET的新关键字: 11 | 12 | \url{https://gitlab.kitware.com/cmake/cmake/-/issues/22468#note_991860} 13 | 14 | \item 15 | 如何安装共享库: 16 | 17 | \url{https://tldp.org/HOWTO/Program-Library-HOWTO/shared-libraries.html} 18 | 19 | \item 20 | 创建可重定位的包: 21 | 22 | \url{https://cmake.org/cmake/help/latest/guide/importing-exporting/index.html#creating-relocatable-packages} 23 | 24 | \item 25 | find\_package()搜索配置文件时扫描的路径列表: 26 | 27 | \url{https://cmake.org/cmake/help/latest/command/find_package.html#configmode-search-procedure} 28 | 29 | \item 30 | CMakePackageConfigHelpers的完整文档: 31 | 32 | \url{https://cmake.org/cmake/help/latest/module/CMakePackageConfigHelpers.html} 33 | 34 | \item 35 | CPack包生成器: 36 | 37 | \url{https://cmake.org/cmake/help/latest/manual/cpack-generators.7.html} 38 | 39 | \item 40 | 关于不同平台首选包生成器的讨论: 41 | 42 | \url{https://stackoverflow.com/a/46013099} 43 | 44 | \item 45 | CPack实用模块文档: 46 | 47 | \url{https://cmake.org/cmake/help/latest/module/CPack.html} 48 | \end{itemize} -------------------------------------------------------------------------------- /book/content/chapter14/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter14/images/1.png -------------------------------------------------------------------------------- /book/content/chapter15/0.tex: -------------------------------------------------------------------------------- 1 | 我们已经汇集了构建专业项目所需的所有必要知识,包括结构化、构建、依赖管理、测试、分析、安装和打包等方面。现在,是时候运用这些技能来创建一个连贯且专业的项目了。重要的是要理解,即使是简单的程序也能从自动化的质量检查,将原始代码转变为完整解决方案的无缝流程中获益。确实,实施这些检查和流程是一项重大的投资,这需要许多步骤来正确地设置一切。当将这些机制添加到现有的代码库时尤其如此,这些代码库往往庞大且复杂。因此,从一开始就使用 CMake 并尽早建立所有必要的过程是非常有益的。这样配置起来更容易,也更高效,因为这些质量控制和构建自动化最终无论如何都需要集成到长期项目中。 2 | 3 | 本章中,将开发一个新的解决方案,尽可能地保持简单,同时充分利用本书迄今为止讨论过的 CMake 实践。为了简化问题,仅实现一个实用的功能——即两个数相加。这样的基础业务代码可使我们能够专注于前面章节中学到的与构建相关的项目方面。为了处理一个与构建更加相关的挑战性问题,此项目将包含一个库和一个可执行文件。 4 | 5 | 该库将处理内部业务逻辑,并作为 CMake 包供其他项目使用。而可执行文件,旨在供最终用户使用,将提供一个用户界面来展示该库的功能。 6 | 7 | 本章中,将包含以下内容: 8 | 9 | \begin{itemize} 10 | \item 11 | 规划工作 12 | 13 | \item 14 | 项目布局 15 | 16 | \item 17 | 构建和管理依赖项 18 | 19 | \item 20 | 测试和程序分析 21 | 22 | \item 23 | 安装和打包 24 | 25 | \item 26 | 提供文档 27 | \end{itemize} -------------------------------------------------------------------------------- /book/content/chapter15/1.tex: -------------------------------------------------------------------------------- 1 | 可以在 GitHub 上找到本章中存在的代码文件,地址为 \url{https://github.com/PacktPublishing/Modern-CMake-for-Cpp-2E/tree/main/examples/ch15}。 2 | 3 | 为了构建本书提供的示例,请使用推荐的命令: 4 | 5 | \begin{shell} 6 | cmake -B <build tree> -S <source tree> 7 | cmake --build <build tree> 8 | \end{shell} 9 | 10 | 请确保将占位符 <build tree> 和 <source tree> 替换为适当的路径。 11 | 作为提醒:<build tree> 是指向目标/输出目录的路径,而 <source tree> 是源代码所在的位置的路径。 12 | 13 | 本章使用 GCC 编译,以提供与使用 lcov 工具收集结果的代码覆盖率仪器兼容性。如果想使用 llvm 或其他工具链进行编译,请确保根据需要调整覆盖率处理过程。 14 | 15 | 要运行测试,请执行以下命令: 16 | 17 | \begin{shell} 18 | ctest --test-dir <build tree> 19 | \end{shell} 20 | 21 | 或者,只需从 build tree 目录执行: 22 | 23 | \begin{shell} 24 | ctest 25 | \end{shell} 26 | 27 | 本章中,测试结果将输出到 test 子目录中。 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /book/content/chapter15/2.tex: -------------------------------------------------------------------------------- 1 | 本章将构建的软件并不复杂——将创建一个简单的计算器,可以实现两个数字的相加(图15.1)。这是一个控制台应用程序,具有文本用户界面,利用第三方库和独立的计算库,这些库可以用于其他项目。尽管这个项目可能没有重要的实际应用,但其简单性非常适合演示本书讨论的各种技术应用。 2 | 3 | \myGraphic{0.4}{content/chapter15/images/1.png}{图15.1:项目在支持鼠标的终端中执行的文本用户界面} 4 | 5 | 通常,项目要么生成面向用户的可执行文件,要么为开发者生成库。项目同时产生这两者的情况较少,尽管这种情况确实存在。例如,一些应用程序附带了独立的SDK或库以帮助开发插件。另一个例子是附带了使用示例的库。我们的项目属于后者,展示了库的功能。 6 | 7 | 我们将通过回顾章节列表,回忆每个章节的内容,并选择将用来构建应用程序的技术和工具来开始规划: 8 | 9 | \begin{itemize} 10 | \item 11 | 第1章,CMake入门: 12 | 13 | 本章提供了关于CMake的基本细节,包括安装和用于构建项目的命令行使用。还包含了关于项目文件的基本信息,如作用、典型的命名约定和特殊性。 14 | 15 | \item 16 | 第2章,CMake语言: 17 | 18 | 我们介绍了编写正确的CMake列表文件和脚本所需的工具,包含了代码基础,如注释、命令调用和参数。我们解释了变量、列表和控制结构,引入了几个有用的命令。这个基础在我们的项目中至关重要。 19 | 20 | \item 21 | 第3章,在IDE中使用CMake: 22 | 23 | 我们讨论了三个IDE——CLion、VS Code和Visual Studio IDE,介绍了它们的优点。在最终项目中,选择IDE(或不选择)由你决定。做出决定后,可以在这个项目中使用Dev容器开始,只需几个步骤就可以构建Docker镜像(或者直接从Docker Hub获取)。在容器中运行镜像可以确保开发环境与生产环境相匹配。 24 | 25 | \item 26 | 第4章,设置CMake项目: 27 | 28 | 配置项目至关重要,其决定了将生效的CMake策略、命名、版本控制和编程语言。我们将使用本章来影响构建过程的基本行为。 29 | 30 | 我们还将遵循建立的项目分区和结构来确定目录和文件的布局,并利用系统发现变量以适应不同的构建环境。工具链配置是另一个关键方面,可以强制使用特定的C++版本和编译器支持的标准。按照章节的建议,我们将禁用源内构建以保持工作区清洁。 31 | 32 | \item 33 | 第5章,使用目标: 34 | 35 | 了解到每个现代CMake项目都广泛使用目标,当然也会应用目标来定义一些库和可执行文件(用于测试和生产),这将使项目保持组织并确保我们遵循DRY(不要重复自己)的原则。对目标属性和传递使用要求(传播属性)的了解,将能够使配置接近目标定义。 36 | 37 | \item 38 | 第6章,使用生成器表达式: 39 | 40 | 生成器表达式在项目中大量使用,力求使这些表达式尽可能简单。项目将包含自定义命令以生成Valgrind和覆盖率报告的文件。此外,还将使用目标钩子,特别是PRE\_BUILD,来清理覆盖率检测过程产生的.gcda文件。 41 | 42 | \item 43 | 第7章,使用CMake编译C++源文件: 44 | 45 | 没有C++项目的编译是不可能的。基础知识相当简单,但CMake允许我们以许多方式调整这个过程:扩展目标源代码、配置优化器并提供调试信息。对于这个项目,默认的编译标志就可以了,也研究了一下预处理器: 46 | 47 | \begin{itemize} 48 | \item 49 | 我们将构建元数据(项目版本、构建时间和Git提交SHA)存储在编译后的可执行文件中并向用户展示。 50 | 51 | \item 52 | 我们将启用预编译头文件。在如此小的项目中,这并不是真正必要的,但它将帮助我们练习这个概念。 53 | \end{itemize} 54 | 55 | 不需要Unity构建。 56 | 57 | \item 58 | 第8章,链接可执行文件和库: 59 | 60 | 我们将获得默认情况下对项目都有用的链接的一般信息。此外,由于这个项目包含一个库,将明确引用以下特定构建指令: 61 | 62 | \begin{itemize} 63 | \item 64 | 用于测试和开发的静态库 65 | 66 | \item 67 | 用于发布的共享库 68 | \end{itemize} 69 | 70 | 本章还概述了如何隔离main()函数以用于测试目的,我们将采用这种做法。 71 | 72 | \item 73 | 第9章,管理依赖关系: 74 | 75 | 为了增强项目的吸引力,将引入一个外部依赖:一个基于文本的用户界面库。第9章探讨了管理依赖关系的各种方法。选择将很简单:FetchContent实用模块通常推荐且最方便。 76 | 77 | \item 78 | 第10章,使用C++20模块: 79 | 80 | 尽管我们已经探讨了使用C++20模块,以及支持此功能的环境要求(CMake 3.28,最新编译器),但其广泛支持仍然不足。为了确保项目的可访问性,我们暂时不会引入模块。 81 | 82 | \item 83 | 第11章:测试框架 84 | 85 | 实施适当的自动化测试,对于确保解决方案质量随时间保持一致至关重要。我们将集成CTest并组织项目以方便测试,并应用之前提到的main()函数分离方法。 86 | 87 | 本章将讨论两种测试框架:Catch2和GTest与GMock;我们将使用后者。为了获取覆盖率的详细信息,我们将使用LCOV生成HTML报告。 88 | 89 | \item 90 | 第12章:程序分析工具 91 | 92 | 对于静态分析,可以从一系列工具中选择:Clang-Tidy、Cpplint、Cppcheck、include-what-you-use(IWYU)和link-what-you-use(LWYU)。我们将选择Cppcheck,因为Clang-Tidy与使用GCC构建的预编译头文件兼容性较差。 93 | 94 | 动态分析将使用Valgrind的Memcheck工具,并配合Memcheck-cover包装器来生成HTML报告。此外,在构建过程中,源码将自动通过ClangFormat进行格式化。 95 | 96 | \item 97 | 第13章:文档生成 98 | 99 | 提供文档对于我们项目中的库来说是必不可少的。CMake支持使用Doxygen自动化生成文档。我们将采用这种方法,并在设计中加入doxygen-awesome-css主题以更新样式。 100 | 101 | \item 102 | 第14章:安装与打包 103 | 104 | 最后,将配置解决方案的安装和打包,并准备文件形成包,包括目标定义。安装这些内容及构建目标产生的工件到合适的目录中,通过包含GNUInstallDirs模块实现。还将配置一些组件以模块化解决方案,并为CPack做好准备。 105 | \end{itemize} 106 | 107 | 专业的项目通常会附带一些文本文件:README、LICENSE、INSTALL等。我们将在章节末尾简要介绍这些文件。 108 | 109 | 为了简化流程,不会实现自定义逻辑来检查所有必需的工具和依赖项是否可用。我们将依赖于CMake来显示其诊断信息并告诉用户缺少什么。如果项目获得了重要的关注,可能需要考虑添加这些机制以改善用户体验。 110 | 111 | 有了清晰的计划后,让我们讨论如何实际地构建项目结构,包括逻辑目标和目录结构。 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /book/content/chapter15/3.tex: -------------------------------------------------------------------------------- 1 | 2 | 为了构建任何项目,应该首先明确项目内部将创建哪些逻辑目标。这个案例中,将遵循下图所示的结构: 3 | 4 | \myGraphic{0.8}{content/chapter15/images/2.png}{图 15.2: 逻辑目标的结构} 5 | 6 | 按照构建顺序来探索这个结构。首先,编译calc\_obj,一个对象库。然后,将注意力转向静态库和共享库。 7 | 8 | \mySubsubsection{15.3.1.}{共享库与静态库} 9 | 10 | 第8章中,介绍了共享库和静态库。当多个程序使用相同的库时,共享库可以减少整体内存使用量,用户通常已经安装了流行的库或者知道如何快速安装它们。 11 | 12 | 更重要的是,共享库是独立的文件,必须放置在特定路径以便动态链接器能够找到它们。相比之下,静态库直接嵌入到可执行文件中,这使得使用更快,不需要额外步骤在内存中定位代码。 13 | 14 | 作为库的作者,可以决定提供静态库,还是共享库,也可以同时发布两个版本,将这个决定留给使用库的开发者。既然在积累知识,就来提供两个版本的库。 15 | 16 | calc\_test目标包含了单元测试以验证库的核心功能,它将使用静态库。虽然是从相同的对象文件构建两种类型的库,但无论使用哪种类型的库进行测试都可以接受,它们的功能相同。与calc\_console\_static目标相关的控制台应用程序将使用共享库。此目标还链接了一个外部依赖,即Arthur Sonzogni的Functional Terminal (X) User Interface (FTXUI)库(在扩展阅读部分中有GitHub项目的链接)。 17 | 18 | 最后两个目标,calc\_console和calc\_console\_test,旨在解决测试可执行文件中的常见问题:测试框架和可执行文件提供的多个入口点之间的冲突。为此,可将main()函数隔离到一个引导目标calc\_console中,该目标仅仅调用calc\_console\_static中的主要函数。 19 | 20 | 理解了必要的目标及其相互关系之后,下一步是通过适当的文件和目录来组织项目的结构。 21 | 22 | \mySubsubsection{15.3.2.}{项目文件结构} 23 | 24 | 项目由两个关键元素组成:calc库和calc\_console可执行文件。为了有效地组织项目,将采用以下目录结构: 25 | 26 | \begin{itemize} 27 | \item 28 | src 包含所有发布的目标的源代码和库头文件。 29 | 30 | \item 31 | test 包含上述库和可执行文件的测试。 32 | 33 | \item 34 | cmake 包含用于构建和安装项目的CMake辅助模块和文件。 35 | 36 | \item 37 | 根目录包含顶级配置和文档文件。 38 | \end{itemize} 39 | 40 | 这一结构(如图15.3所示)确保了职责的清晰划分,便于更轻松地导航和维护项目: 41 | 42 | \myGraphic{0.8}{content/chapter15/images/3.png}{图 15.3: 项目的目录结构} 43 | 44 | 下面是每个主要目录中的文件完整列表: 45 | 46 | % Please add the following required packages to your document preamble: 47 | % \usepackage{longtable} 48 | % Note: It may be necessary to compile the document several times to get a multi-page table to line up properly 49 | \begin{longtable}{|l|l|} 50 | \hline 51 | \textbf{根目录} & 52 | \textbf{./test} \\ \hline 53 | \endfirsthead 54 | % 55 | \endhead 56 | % 57 | CHANGELOG & 58 | CMakeLists.txt \\ \hline 59 | \textbf{CMakeLists.txt} & 60 | \textbf{calc/CMakeLists.txt} \\ \hline 61 | \begin{tabular}[c]{@{}l@{}}INSTALL\\ LICENSER\\ EADME.md\end{tabular} & 62 | \begin{tabular}[c]{@{}l@{}}calc/calc\_test.cpp\\ calc\_console/CMakeLists.txt\\ calc\_console/tui\_test.cpp\end{tabular} \\ \hline 63 | \textbf{./src} & 64 | \textbf{./cmake} \\ \hline 65 | \begin{tabular}[c]{@{}l@{}}CMakeLists.txt\\ calc/CMakeLists.txt\\ calc/CalcConfig.cmake\\ calc/basic.cpp\\ calc/include/calc/basic.h\\ calc\_console/CMakeLists.txt\\ calc\_console/bootstrap.cpp\\ calc\_console/include/tui.h\\ calc\_console/tui.cpp\end{tabular} & 66 | \begin{tabular}[c]{@{}l@{}}BuildInfo.cmake\\ Coverage.cmake\\ CppCheck.cmake\\ Doxygen.cmake\\ Format.cmake\\ GetFTXUI.cmake\\ Packaging.cmake\\ Memcheck.cmake\\ NoInSourceBuilds.cmake\\ Testing.cmake\\ buildinfo.h.in\\ doxygen\_extra\_headers\end{tabular} \\ \hline 67 | \end{longtable} 68 | 69 | \begin{center} 70 | 表 15.1: 项目的文件结构 71 | \end{center} 72 | 73 | 看起来CMake引入了相当大的开销,初始阶段cmake目录包含的内容比实际业务代码还要多,但随着项目的扩展,这种状况将会改变。建立干净且有组织的项目结构需要较大的初始努力,但请放心,这种投资在未来将带来显著的好处。 74 | 75 | 我们将在整个章节中详细介绍表15.1中列出的所有文件,以及其作用和在项目中的角色。这将分为四个步骤:构建、测试、安装和提供文档。 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /book/content/chapter15/6.tex: -------------------------------------------------------------------------------- 1 | 2 | 图15.6展示了如何配置项目以便进行安装和打包: 3 | 4 | \myGraphic{1.0}{content/chapter15/images/6.png}{图15.6: 配置安装和打包的文件} 5 | 6 | 顶层列表文件包含了Packaging模块: 7 | 8 | \filename{ch15/01-full-project/CMakeLists�txt (片段)} 9 | 10 | \begin{cmake} 11 | # ... configure project 12 | # ... enable testing 13 | # ... include src and test subdirectories 14 | 15 | include(Packaging) 16 | \end{cmake} 17 | 18 | Packaging模块详细说明了项目的包配置,这部分将在使用CPack进行打包的部分进行探讨。现在,关注的是安装三个主要组件: 19 | 20 | \begin{itemize} 21 | \item 22 | Calc库工件:静态和共享库、头文件以及目标导出文件 23 | 24 | \item 25 | Calc库的包定义配置文件 26 | 27 | \item 28 | Calc控制台可执行文件 29 | \end{itemize} 30 | 31 | 一切已经计划好了,现在是时候安装库。 32 | 33 | \mySubsubsection{15.6.1.}{安装库} 34 | 35 | 为了安装库,首先定义逻辑目标及其工件的目的地,利用GNUInstallDirs模块的默认值以避免手动指定路径。工件将分组到组件中,默认安装会安装所有文件,可以选择只安装运行时组件,并跳过开发工件: 36 | 37 | \filename{ch15/01-full-project/src/calc/CMakeLists.txt (续)} 38 | 39 | \begin{cmake} 40 | # ... calc library targets definition 41 | # ... configuration, testing, program analysis 42 | 43 | # Installation 44 | include(GNUInstallDirs) 45 | install(TARGETS calc_obj calc_shared calc_static 46 | EXPORT CalcLibrary 47 | ARCHIVE COMPONENT development 48 | LIBRARY COMPONENT runtime 49 | FILE_SET HEADERS COMPONENT runtime 50 | ) 51 | \end{cmake} 52 | 53 | 对于UNIX系统,我们还配置了安装后注册共享库到ldconfig: 54 | 55 | \filename{ch15/01-full-project/src/calc/CMakeLists.txt (续)} 56 | 57 | \begin{cmake} 58 | if (UNIX) 59 | install(CODE "execute_process(COMMAND ldconfig)" 60 | COMPONENT runtime 61 | ) 62 | endif() 63 | \end{cmake} 64 | 65 | 为了使其他CMake项目能够复用,将通过生成并安装一个目标导出文件和一个引用它的配置文件来打包库: 66 | 67 | \filename{ch15/01-full-project/src/calc/CMakeLists.txt (续)} 68 | 69 | \begin{cmake} 70 | install(EXPORT CalcLibrary 71 | DESTINATION ${CMAKE_INSTALL_LIBDIR}/calc/cmake 72 | NAMESPACE Calc:: 73 | COMPONENT runtime 74 | ) 75 | 76 | install(FILES "CalcConfig.cmake" 77 | DESTINATION ${CMAKE_INSTALL_LIBDIR}/calc/cmake 78 | ) 79 | \end{cmake} 80 | 81 | 为了简化,CalcConfig.cmake文件保持最小化: 82 | 83 | \filename{ch15/01-full-project/src/calc/CalcConfig.cmake} 84 | 85 | \begin{cmake} 86 | include("${CMAKE_CURRENT_LIST_DIR}/CalcLibrary.cmake") 87 | \end{cmake} 88 | 89 | 这个文件位于src/calc目录下,它只包含了库目标。如果还有其他目录的目标定义,比如calc\_console,通常会把CalcConfig.cmake放在顶层目录或src目录下。 90 | 91 | 现在,库已经准备好通过cmake -{}-install命令在构建项目后进行安装。不过,还需要配置可执行文件的安装。 92 | 93 | \mySubsubsection{15.6.2.}{可执行文件的安装} 94 | 95 | 当然,我们希望用户能够在他们的系统中使用可执行文件,因此需要使用CMake来安装。准备二进制可执行文件的安装很简单;为了实现这一点,只需要包含GNUInstallDirs并使用install()命令: 96 | 97 | \filename{ch15/01-full-project/src/calc\_console/CMakeLists.txt (续)} 98 | 99 | \begin{cmake} 100 | # ... calc_console_static definition 101 | # ... configuration, testing, program analysis 102 | # ... calc_console bootstrap executable definition 103 | 104 | # Installation 105 | include(GNUInstallDirs) 106 | install(TARGETS calc_console 107 | RUNTIME COMPONENT runtime 108 | ) 109 | \end{cmake} 110 | 111 | 这样,可执行文件就设置好进行安装了。现在,继续进行打包。 112 | 113 | \mySubsubsection{15.6.3.}{使用CPack打包} 114 | 115 | 可以尽情配置大量的支持的包类型;但对于这个项目,基本配置就够了: 116 | 117 | \filename{ch15/01-full-project/cmake/Packaging.cmake} 118 | 119 | \begin{cmake} 120 | # CPack configuration 121 | set(CPACK_PACKAGE_VENDOR "Rafal Swidzinski") 122 | set(CPACK_PACKAGE_CONTACT "email@example.com") 123 | set(CPACK_PACKAGE_DESCRIPTION "Simple Calculator") 124 | include(CPack) 125 | \end{cmake} 126 | 127 | 这样的最小化配置对于标准存档,如ZIP文件,工作得很好。为了在构建项目后测试安装和打包过程,请在构建树内使用以下命令: 128 | 129 | \begin{shell} 130 | # cpack -G TGZ -B packages 131 | CPack: Create package using TGZ 132 | CPack: Install projects 133 | CPack: - Run preinstall target for: Calc 134 | CPack: - Install project: Calc [] 135 | CPack: Create package 136 | CPack: - package: .../packages/Calc-1.0.0-Linux.tar.gz generated. 137 | \end{shell} 138 | 139 | 这就完成了安装和打包的过程;接下来的任务是提供文档。 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /book/content/chapter15/8.tex: -------------------------------------------------------------------------------- 1 | 本章中,基于目前为止所学的内容构建了一个专业项目。让我们快速回顾一下。 2 | 3 | 首先规划了项目的布局,并讨论了哪些文件将位于哪个目录中。基于以往的经验,以及想要实践更高级场景的愿望,界定了一个面向用户的主应用程序和一个其他开发者可能使用的库。这塑造了目录的结构,以及希望构建的 CMake 目标之间的关系。随后,配置了各个构建目标:提供了库的源代码,定义了它的目标,并设置了使用位置独立代码参数供使用。面向用户的应用程序也可以定义其可执行目标,提供了源代码,并配置了其依赖项:FTXUI 库。 4 | 5 | 有了待构建的制品之后,继续增强了项目的测试和质量保证功能。添加了覆盖率模块来生成覆盖率报告,使用 Memcheck 在运行时通过 Valgrind 验证解决方案,并执行了静态分析 CppCheck。 6 | 7 | 这样一个项目现在已经准备好安装,所以我们使用学到的技术为库和可执行文件创建了适当的安装条目,并为 CPack 准备了包配置。最后的任务是确保项目文档正确无误,因此设置了使用 Doxygen 自动生成文档,并编写了几份基本文档来处理软件分发中不太技术性的方面。 8 | 9 | 这就完成了项目配置,并且现在可以轻松地使用几个精确的 CMake 命令来构建和安装项目。但如果我们可以只用一个简单的命令来完成整个过程呢?让我们在最后一章:第 16 章中探索这个主题。 -------------------------------------------------------------------------------- /book/content/chapter15/9.tex: -------------------------------------------------------------------------------- 1 | 2 | \begin{itemize} 3 | \item 4 | 构建静态库和共享库的相关信息: 5 | 6 | \url{https://stackoverflow.com/q/2152077} 7 | 8 | \item 9 | FXTUI 库项目: 10 | 11 | \url{https://github.com/ArthurSonzogni/FTXUI} 12 | 13 | \item 14 | option() 命令的文档: 15 | 16 | \url{https://cmake.org/cmake/help/latest/command/option.html} 17 | 18 | \item 19 | Google 关于开源软件发布的准备指南: 20 | 21 | \url{https://opensource.google/docs/releasing/preparing/} 22 | 23 | \item 24 | 为什么不能在 GCC 预编译头文件中使用 Clang-Tidy: 25 | 26 | \url{https://gitlab.kitware.com/cmake/cmake/-/issues/22081#note_943104} 27 | 28 | \item 29 | Cppcheck 手册: 30 | 31 | \url{https://cppcheck.sourceforge.io/manual.pdf} 32 | 33 | \item 34 | 如何撰写 README 文件: 35 | 36 | \url{https://www.freecodecamp.org/news/how-to-write-a-good-readme-file/} 37 | 38 | \item 39 | GitHub 项目的 Creative Commons 许可证: 40 | 41 | \url{https://github.com/santisoler/cc-licenses} 42 | 43 | \item 44 | GitHub 认可的常用项目许可证: 45 | 46 | \url{https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/licensing-a-repository} 47 | \end{itemize} 48 | -------------------------------------------------------------------------------- /book/content/chapter15/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter15/images/1.png -------------------------------------------------------------------------------- /book/content/chapter15/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter15/images/2.png -------------------------------------------------------------------------------- /book/content/chapter15/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter15/images/3.png -------------------------------------------------------------------------------- /book/content/chapter15/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter15/images/4.png -------------------------------------------------------------------------------- /book/content/chapter15/images/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter15/images/5.png -------------------------------------------------------------------------------- /book/content/chapter15/images/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter15/images/6.png -------------------------------------------------------------------------------- /book/content/chapter15/images/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter15/images/7.png -------------------------------------------------------------------------------- /book/content/chapter16/0.tex: -------------------------------------------------------------------------------- 1 | CMake 3.19 版本中引入了预设(Presets),目的是简化项目设置的管理。预设出现之前,用户需要记住冗长的命令行配置,或是直接在项目文件中设置覆盖选项,这往往会变得复杂且容易出错。预设让用户能够以更简单的方式处理,诸如用于配置项目的生成器、并发构建任务的数量,以及要构建或测试的项目组件等设置。通过使用预设,CMake 变得更加易于使用。用户可以一次性设置预设,并在需要时使用它们,从而使每次 CMake 执行更加一致且易于理解。还有助于跨不同用户和计算机标准化设置,从而简化协作项目的工作流程。 2 | 3 | 预设兼容 CMake 的四种主要模式:配置构建系统、构建、运行测试和打包。允许用户将这些部分链接在一起形成工作流,使整个过程更加自动化和有序。此外,预设提供了如条件和宏表达式(或简称宏)等功能,给予用户更大的控制力。 4 | 5 | 本章中,将包含以下内容: 6 | 7 | \begin{itemize} 8 | \item 9 | 使用项目中定义的预设 10 | 11 | \item 12 | 编写预设文件 13 | 14 | \item 15 | 定义特定阶段的预设 16 | 17 | \item 18 | 定义工作流预设 19 | 20 | \item 21 | 添加条件和宏 22 | \end{itemize} 23 | 24 | 25 | -------------------------------------------------------------------------------- /book/content/chapter16/1.tex: -------------------------------------------------------------------------------- 1 | 2 | 可以在 GitHub 上找到本章中存在的代码文件,地址是 \url{https://github.com/PacktPublishing/Modern-CMake-for-Cpp-2E/tree/main/examples/ch16}。 3 | 4 | 执行本章示例所需的命令将在每个章节中提供。 -------------------------------------------------------------------------------- /book/content/chapter16/2.tex: -------------------------------------------------------------------------------- 1 | 项目的配置可能成为一个复杂的任务,特别是需要具体指定缓存变量、选定的生成器等元素时——尤其是当项目有多种构建方式的时候,这时预设就变得非常有用。不必记住命令行参数或是编写 shell 脚本,来以不同的参数运行 cmake,而是可以创建一个预设文件,并将所需的配置存储在项目中。 2 | 3 | CMake 使用两个可选文件来存储项目预设: 4 | 5 | \begin{itemize} 6 | \item 7 | CMakePresets.json: 由项目作者提供的官方预设。 8 | 9 | \item 10 | CMakeUserPresets.json: 专为希望向项目添加自定义预设的用户设计。项目应当将此文件添加到版本控制系统忽略列表中,以确保自定义设置不会共享到仓库中。 11 | \end{itemize} 12 | 13 | 预设文件必须放置在项目的顶层目录中以便 CMake 能够识别。每个预设文件可以为每个阶段定义多个预设:配置 (configure)、构建 (build)、测试 (test)、打包 (package),以及涵盖多个阶段的工作流 (workflow) 预设。然后用户可以通过集成开发环境 (IDE)、图形用户界面 (GUI) 或者命令行选择并执行一个预设。 14 | 15 | 预设可以通过在命令行中添加 -{}-list-presets 参数来列出,具体取决于我们要列出的阶段。例如,列出构建预设可以用下面的命令: 16 | 17 | \begin{shell} 18 | cmake --build --list-presets 19 | \end{shell} 20 | 21 | 列出测试预设可以用下面的命令: 22 | 23 | \begin{shell} 24 | ctest --list-presets 25 | \end{shell} 26 | 27 | 要使用一个预设,需要遵循相同的模式,并在 -{}-preset 参数之后提供预设名称。 28 | 29 | 另外,不能使用 cmake 命令来列出打包预设,需要使用 cpack。这里是一个用于打包预设的命令行示例: 30 | 31 | \begin{shell} 32 | cpack --preset <preset-name> 33 | \end{shell} 34 | 35 | 选择好预设后,还可以添加特定于阶段的命令行参数,例如指定构建树或者安装路径。添加的参数会覆盖预设中设置的内容。 36 | 37 | 工作流预设有特殊情况,运行 cmake 命令时如果存在 -{}-workflow 参数,则可以列出并应用工作流预设: 38 | 39 | \begin{shell} 40 | $ cmake --workflow --list-presets 41 | Available workflow presets: 42 | "myWorkflow" 43 | $ cmake --workflow --preset myWorkflow 44 | Executing workflow step 1 of 4: configure preset "myConfigure" 45 | ... 46 | \end{shell} 47 | 48 | 这就是如何在项目中应用和查看可用预设的方法。现在,让我们探索一下如何构建预设文件。 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /book/content/chapter16/3.tex: -------------------------------------------------------------------------------- 1 | CMake 在项目的顶层目录中搜索 CMakePresets.json 和 CMakeUserPresets.json 文件。这两个文件使用相同的 JSON 结构来定义预设,它们之间的区别不大。其格式是一个 JSON 对象,包含以下键: 2 | 3 | \begin{itemize} 4 | \item 5 | version: 这是一个必需的整数,指定了预设 JSON 架构的版本。 6 | 7 | \item 8 | cmakeMinimumRequired: 这是一个对象,指定了所需的 CMake 版本。 9 | 10 | \item 11 | include: 这是一个字符串数组,从数组中提供的文件路径包含外部预设(自第 4 版架构开始)。 12 | 13 | \item 14 | configurePresets: 这是一个对象数组,定义了配置阶段的预设。 15 | 16 | \item 17 | buildPresets: 这是一个对象数组,定义了构建阶段的预设。 18 | 19 | \item 20 | testPresets: 这是一个对象数组,专门针对测试阶段的预设。 21 | 22 | \item 23 | packagePresets: 这是一个对象数组,专门针对打包阶段的预设。 24 | 25 | \item 26 | workflowPresets: 这是一个对象数组,专门针对工作流模式的预设。 27 | 28 | \item 29 | vendor: 这是一个对象,包含由 IDE 和其他供应商定义的自定义设置;CMake 不处理这个字段。 30 | \end{itemize} 31 | 32 | 编写预设时,CMake 要求存在 version 入口;其他值则可选。下面是一个预设文件的例子(实际的预设将在后续部分添加): 33 | 34 | \filename{ch16/01-presets/CMakePresets.json} 35 | 36 | \begin{json} 37 | { 38 | "version": 6, 39 | "cmakeMinimumRequired": { 40 | "major": 3, 41 | "minor": 26, 42 | "patch": 0 43 | }, 44 | "include": [], 45 | "configurePresets": [], 46 | "buildPresets": [], 47 | "testPresets": [], 48 | "packagePresets": [], 49 | "workflowPresets": [], 50 | "vendor": { 51 | "data": "IDE-specific information" 52 | } 53 | } 54 | \end{json} 55 | 56 | 上述例子中,并不需要添加空数组;除了 version 以外的条目都是可选的。顺便说一下,对于 CMake 3.26 的适当架构版本是 6。 57 | 58 | 已经了解了预设文件的结构,接下来我们就学习如何实际定义这些预设。 59 | -------------------------------------------------------------------------------- /book/content/chapter16/5.tex: -------------------------------------------------------------------------------- 1 | 工作流预设是我们项目的终极自动化解决方案,允许我们按照预定顺序自动执行多个特定阶段的预设。这样,实际上可以用一步完成端到端的构建。 2 | 3 | 要发现项目的可用工作流,我们可以执行以下命令: 4 | 5 | \begin{shell} 6 | cmake --workflow --list-presets 7 | \end{shell} 8 | 9 | 要选择并应用一个预设,使用以下命令: 10 | 11 | \begin{shell} 12 | cmake --workflow --preset <preset-name> 13 | \end{shell} 14 | 15 | 此外,使用 -{}-fresh 标志,可以清除构建树和缓存。 16 | 17 | 定义工作流预设非常简单;需要定义一个名称,并且可以像为特定阶段的预设那样可选地提供 displayName 和 description。之后,必须枚举出工作流应该执行的所有特定阶段的预设。这是通过提供一个 steps 数组来完成的,数组中的对象包含 type 和 name 属性: 18 | 19 | \filename{ch16/01-presets/CMakePresets.json (续)} 20 | 21 | \begin{json} 22 | ... 23 | "workflowPresets": [ 24 | { 25 | "name": "myWorkflow", 26 | "steps": [ 27 | { 28 | "type": "configure", 29 | "name": "myConfigure" 30 | }, 31 | { 32 | "type": "build", 33 | "name": "myBuild" 34 | }, 35 | { 36 | "type": "test", 37 | "name": "myTest" 38 | }, 39 | { 40 | "type": "package", 41 | "name": "myPackage" 42 | }, 43 | { 44 | "type": "build", 45 | "name": "myInstall" 46 | } 47 | ] 48 | ... 49 | \end{json} 50 | 51 | steps 数组中的每个对象引用了本章中早些时候定义的预设,指示其类型(configure, build, test, 或 package)和名称。这些预设一起执行所有必要的步骤,只需一条命令即可从头开始完全构建和安装项目: 52 | 53 | \begin{shell} 54 | cmake --workflow --preset myWorkflow 55 | \end{shell} 56 | 57 | 工作流预设是用于自动化 C++ 构建、测试、打包和安装的解决方案。接下来,让我们探讨如何通过条件和宏来管理一些特殊情况。 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /book/content/chapter16/6.tex: -------------------------------------------------------------------------------- 1 | 当讨论每个特定阶段预设的一般字段时,提到了 condition 字段。现在是回到这个话题的时候了。condition 字段可以启用或禁用一个预设,在与工作流集成时揭示其真正的潜力,它允许在某些条件下绕过不适合的预设,并创建替代的预设。 2 | 3 | 条件要求预设架构版本 3 或以上(在 CMake 3.22 中引入),并且是 JSON 对象,编码了一些简单的逻辑操作,可以根据所使用的操作系统、环境变量甚至选定的生成器等条件来判断预设是否适用。CMake 通过宏提供了这些数据,这些宏基本上是一组只读变量,可以在预设文件中使用。 4 | 5 | condition 对象的结构根据检查类型的不同而变化。每个条件都必须包含一个 type 字段,以及由该类型定义的其他字段。其基本类型包括: 6 | 7 | \begin{itemize} 8 | \item 9 | const: 这个检查 value 字段中提供的值是否为布尔真。 10 | 11 | \item 12 | equals 和 notEquals: 比较 lhs 字段的值与 rhs 字段的值。 13 | 14 | \item 15 | inList 和 notInList: 这些检查 string 字段中提供的值是否存在于 list 字段的数组中。 16 | 17 | \item 18 | matches 和 notMatches: 这些判断 string 字段的值是否符合 regex 字段中定义的模式。 19 | \end{itemize} 20 | 21 | 一个示例条件如下所示: 22 | 23 | \begin{json} 24 | "condition": { 25 | "type": "equals", 26 | "lhs": "${hostSystemName}", 27 | "rhs": "Windows" 28 | } 29 | \end{json} 30 | 31 | const 条件的实际用途,主要是为了在不从 JSON 文件中删除预设的情况下禁用。除了 const 之外,所有基本条件都允许在其引入的字段 (lhs, rhs, string, list, regex) 中使用宏。 32 | 33 | 高级条件类型,类似于“not”,“and”和“or”操作,使用其他条件作为参数: 34 | 35 | \begin{itemize} 36 | \item 37 | not: 这是对 condition 字段中提供的条件进行布尔反转。 38 | 39 | \item 40 | anyOf 和 allOf: 这些检查 conditions 数组中的任意条件或所有条件是否为真。 41 | \end{itemize} 42 | 43 | 例如: 44 | 45 | \begin{json} 46 | "condition": { 47 | "type": "anyOf", 48 | "conditions": [ 49 | { 50 | "type": "equals", 51 | "lhs": "${hostSystemName}", 52 | "rhs": "Windows" 53 | }, 54 | { 55 | "type": "equals", 56 | "lhs": "${hostSystemName}", 57 | "rhs": "Linux" 58 | } 59 | ] 60 | } 61 | \end{json} 62 | 63 | 这个条件在系统为 Linux 或 Windows 时计算为真。 64 | 65 | 通过这些示例,引入了第一个宏:\$\{hostSystemName\}。宏遵循简单的语法,并限于特定实例: 66 | 67 | \begin{itemize} 68 | \item 69 | \$\{sourceDir\}: 源代码树的路径。 70 | 71 | \item 72 | \$\{sourceParentDir\}: 源代码树父目录的路径。 73 | 74 | \item 75 | \$\{sourceDirName\}: 项目的目录名。 76 | 77 | \item 78 | \$\{presetName\}: 预设的名称。 79 | 80 | \item 81 | \$\{generator\}: 用于创建构建系统的生成器。 82 | 83 | \item 84 | \$\{hostSystemName\}: 系统名称:Linux, Windows, 或 macOS 上的 Darwin。 85 | 86 | \item 87 | \$\{fileDir\}: 包含当前预设的文件名(当使用 include 数组导入外部预设时适用)。 88 | 89 | \item 90 | \$\{dollar\}: 转义的美元符号 \$。 91 | 92 | \item 93 | \$\{pathListSep\}: 、环境特定的路径分隔符。 94 | 95 | \item 96 | \$env\{<variable-name>\}: 如果预设指定了环境变量,则返回该环境变量(区分大小写),否则返回父环境中的值。 97 | 98 | \item 99 | \$penv\{<variable-name>\}: 返回父环境中的环境变量。 100 | 101 | \item 102 | \$vendor\{<macro-name>\}: 允许 IDE 厂商引入自己的宏。 103 | \end{itemize} 104 | 105 | 这些宏为在预设及其条件中的使用提供了足够的灵活性,使我们能够根据需要有效地切换工作流步骤。 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /book/content/chapter16/7.tex: -------------------------------------------------------------------------------- 1 | 我们刚刚完成了一个关于 CMake 预设的全面概述,这些预设是从 CMake 3.19 版本开始引入的,旨在简化项目管理。预设让产品作者可以通过配置项目构建和交付的所有阶段来为用户提供整洁的体验。预设不仅简化了 CMake 的使用,还提高了一致性,并允许根据环境进行设置。 2 | 3 | 我们解释了 CMakePresets.json 和 CMakeUserPresets.json 文件的结构和用法,并提供了关于定义各种类型的预设的见解,包括配置预设、构建预设、测试预设、打包预设和工作流预设。每种类型都进行了详细的描述:了解了常见的字段,如何在内部构建预设、建立它们之间的继承关系,以及为最终用户提供特定的配置选项。 4 | 5 | 对于配置预设,讨论了重要的主题,如选择生成器、构建目录和安装目录,并通过 configurePreset 字段将预设链接在一起。现在知道了如何处理构建预设和设置构建作业数量、目标以及清理选项。随后,学习了测试预设如何通过广泛的过滤和排序选项、输出格式化,以及执行参数(如超时和容错)来辅助测试选择。了解了如何通过指定打包生成器、过滤选项,以及打包元数据来管理打包预设。甚至还介绍了一种通过专门的构建预设应用,来执行安装阶段的变通方法。 6 | 7 | 接着,我们发现了工作流预设如何将多个特定阶段的预设组合在一起。最后,讨论了条件和宏表达式,为项目作者提供了对单个预设的行为,及其集成到工作流中的更大控制权。 8 | 9 | 我们的 CMake 之旅至此结束!恭喜你——现在拥有了开发、测试和打包高质量 C++ 软件所需的所有工具。最好的前进方式就是应用你所学的知识去创造优秀的软件给你的用户。祝各位好运! -------------------------------------------------------------------------------- /book/content/chapter16/8.tex: -------------------------------------------------------------------------------- 1 | 2 | \begin{itemize} 3 | \item 4 | 预设的官方文档: 5 | 6 | \url{https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html} 7 | \end{itemize} -------------------------------------------------------------------------------- /book/content/chapter16/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter16/images/1.png -------------------------------------------------------------------------------- /book/content/chapter17/0.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /book/content/chapter17/1.tex: -------------------------------------------------------------------------------- 1 | 2 | 每种编程语言都包含了对各种任务有用的实用命令,CMake 也不例外。它提供了用于算术运算、位操作、字符串处理以及列表和文件操作的工具。尽管由于增强功能和大量模块的发展,这些命令的需求已经减少,但在高度自动化的项目中它们仍然重要。如今,可能会更多地在使用 cmake -P <filename> 调用的 CMake 脚本中发现它们的作用。 3 | 4 | 因此,本附录总结了各种 CMake 命令及其不同的模式,可以作为方便的离线参考,或是官方文档的简化版。对于更详细的信息,建议查阅提供的链接。 5 | 6 | 此参考适用于 CMake 3.26.6 版本。 7 | 8 | 本附录中,包括以下内容: 9 | 10 | \begin{itemize} 11 | \item 12 | string() 13 | 14 | \item 15 | list() 16 | 17 | \item 18 | file() 19 | 20 | \item 21 | math() 22 | \end{itemize} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /book/content/chapter17/3.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 此命令提供了基本的列表操作:读取、查找、修改及排序。某些模式会改变列表(即修改原始值)。如果稍后还需要原始值,请确保先复制一份。 4 | 5 | 完整详情可以在在线文档中找到: 6 | 7 | \url{https://cmake.org/cmake/help/latest/command/list.html} 8 | 9 | 可用的 list() 模式的类别包括读取、查找、修改及排序。 10 | 11 | \mySubsubsection{A.3.1.}{读取} 12 | 13 | 以下是可用的模式: 14 | 15 | \begin{itemize} 16 | \item 17 | list(LENGTH <list> <out>):计算 <list> 变量中的元素数量,并将结果存储在 <out> 变量中。 18 | 19 | \item 20 | list(GET <list> <index>... <out>):将由 <index> 索引列表指定的 <list> 元素复制到 <out> 变量中。 21 | 22 | \item 23 | list(JOIN <list> <glue> <out>):使用 <glue> 分隔符交错 <list> 元素,并将结果字符串存储在 <out> 变量中。 24 | 25 | \item 26 | list(SUBLIST <list> <begin> <length> <out>):其作用类似于 GET 模式,但不是使用显式索引而是使用范围。如果 <length> 是 -1,则从 <begin> 索引到 <list> 变量中的列表末尾的所有元素都会被返回。 27 | \end{itemize} 28 | 29 | \mySubsubsection{A.3.2.}{查找} 30 | 31 | 此模式仅仅是在 <list> 变量中查找 <needle> 元素的索引,并将结果存储在 <out> 变量中(如果没有找到该元素,则存储 -1): 32 | 33 | \begin{cmake} 34 | list(FIND <list> <needle> <out>) 35 | \end{cmake} 36 | 37 | \mySubsubsection{A.3.3.}{修改} 38 | 39 | 以下是可用的模式: 40 | 41 | \begin{itemize} 42 | \item 43 | list(APPEND <list> <element>...) :将一个或多个 <element> 值添加到 <list> 变量的末尾。 44 | 45 | \item 46 | list(PREPEND <list> [<element>...]):其作用类似于 APPEND,但将元素添加到 <list> 变量的开头。 47 | 48 | \item 49 | list(FILTER <list> \{INCLUDE | EXCLUDE\} REGEX <pattern>):根据 <pattern> 值过滤 <list> 变量,以 INCLUDE 或 EXCLUDE 匹配的元素。 50 | 51 | \item 52 | list(INSERT <list> <index> [<element>...]):在给定的 <index> 位置将一个或多个 <element> 值添加到 <list> 变量中。 53 | 54 | \item 55 | list(POP\_BACK <list> [<out>...]):从 <list> 变量的末尾移除一个元素,并将它存储在可选的 <out> 变量中。如果提供了多个 <out> 变量,则会移除更多元素来填充它们。 56 | 57 | \item 58 | list(POP\_FRONT <list> [<out>...]):其作用类似于 POP\_BACK,但从 <list> 变量的开头移除元素。 59 | 60 | \item 61 | list(REMOVE\_ITEM <list> <value>...):是 FILTER EXCLUDE 的简写,但不支持正则表达式。 62 | 63 | \item 64 | list(REMOVE\_AT <list> <index>...):从 <list> 中移除特定 <index> 的元素。 65 | 66 | \item 67 | list(REMOVE\_DUPLICATES <list>):从 <list> 中移除重复项。 68 | 69 | \item 70 | list(TRANSFORM <list> <action> [<selector>] [OUTPUT\_VARIABLE <out>]) :对 <list> 元素应用特定的转换。默认情况下,操作应用于所有元素,但可以通过添加 <selector> 来限制其效果。除非提供 OUTPUT\_VARIABLE 关键词,结果将存储在 <out> 变量中,否则将修改提供的列表(就地更改)。 71 | \end{itemize} 72 | 73 | 以下选择器可用:AT <index>,FOR <start> <stop> [<step>],以及 REGEX <pattern>。 74 | 75 | 操作包括 APPEND <string>,PREPEND <string>,TOLOWER,TOUPPER,STRIP,GENEX\_STRIP,以及 REPLACE <pattern> <expression>。它们的工作方式与具有相同名称的 string() 模式完全相同。 76 | 77 | \mySubsubsection{A.3.4.}{排序} 78 | 79 | 以下是可用的模式: 80 | 81 | \begin{itemize} 82 | \item 83 | list(REVERSE <list>):简单地反转 <list> 的顺序。 84 | 85 | \item 86 | list(SORT <list>):按字母顺序对列表进行排序。 87 | \end{itemize} 88 | 89 | 有关更高级选项的参考,请参阅在线手册。 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /book/content/chapter17/4.tex: -------------------------------------------------------------------------------- 1 | 2 | 此命令提供了与文件相关的各种操作:读取、传输、锁定及归档。还提供了用于检查文件系统和对路径字符串进行操作的模式。 3 | 4 | 完整详情可以在在线文档中找到: 5 | 6 | \url{https://cmake.org/cmake/help/latest/command/file.html} 7 | 8 | 可用的 file() 模式的类别包括读取、写入、文件系统、路径转换、传输、锁定及归档。 9 | 10 | \mySubsubsection{A.4.1.}{读取} 11 | 12 | 以下是可用的模式: 13 | 14 | \begin{itemize} 15 | \item 16 | file(READ <filename> <out> [OFFSET <o>] [LIMIT <max>] [HEX]):从 <filename> 读取文件到 <out> 变量。读取可选地从偏移量 <o> 开始,并遵循最大字节数 <max> 的限制。HEX 标志指明输出应转换为十六进制表示。 17 | 18 | \item 19 | file(STRINGS <filename> <out>):从 <filename> 中读取字符串到 <out> 变量。 20 | 21 | \item 22 | file(<hashing-algorithm> <filename> <out>):计算 <filename> 文件的 <hashing-algorithm> 哈希值,并将结果存储在 <out> 变量中。可用的算法与 string() 哈希函数相同。 23 | 24 | \item 25 | file(TIMESTAMP <filename> <out> [<format>]):生成 <filename> 文件的时间戳字符串表示,并将其存储在 <out> 变量中。可选地接受一个 <format> 字符串。 26 | 27 | \item 28 | file(GET\_RUNTIME\_DEPENDENCIES [...]):获取指定文件的运行时依赖项。这是一个高级命令,只应在 install(CODE) 或 install(SCRIPT) 场景中使用。自 CMake 3.21 开始可用。 29 | \end{itemize} 30 | 31 | \mySubsubsection{A.4.2.}{写入} 32 | 33 | 以下是可用的模式: 34 | 35 | \begin{itemize} 36 | \item 37 | file(\{WRITE | APPEND\} <filename> <content>...):将所有 <content> 参数写入或追加到 <filename> 文件中。如果提供的系统路径不存在,将会递归创建。 38 | 39 | \item 40 | file(\{TOUCH | TOUCH\_NOCREATE\} [<filename>...]):更新 <filename> 的时间戳。如果文件不存在,则仅在 TOUCH 模式下创建。 41 | 42 | \item 43 | file(GENERATE OUTPUT <output-file> [...]):这是一种高级模式,为当前 CMake 生成器的每个构建配置生成输出文件。 44 | 45 | \item 46 | file(CONFIGURE OUTPUT <output-file> CONTENT <content> [...]) :其作用类似于 GENERATE\_OUTPUT,但同时通过替换变量占位符来配置生成的文件。 47 | \end{itemize} 48 | 49 | \mySubsubsection{A.4.3.}{文件系统} 50 | 51 | 以下是可用的模式: 52 | 53 | \begin{itemize} 54 | \item 55 | file(\{GLOB | GLOB\_RECURSE\} <out> [...] [<globbing-expression>...]):生成匹配 <globbing-expression> 的文件列表,并将其存储在 <out> 变量中。GLOB\_RECURSE 模式也会扫描嵌套目录。 56 | 57 | \item 58 | file(RENAME <oldname> <newname>):将文件从 <oldname> 移动到 <newname>。 59 | 60 | \item 61 | file(\{REMOVE | REMOVE\_RECURSE \} [<files>...]):删除 <files>。REMOVE\_RECURSE 也会删除目录。 62 | 63 | \item 64 | file(MAKE\_DIRECTORY [<dir>...]):创建目录。 65 | 66 | \item 67 | file(COPY <file>... DESTINATION <dir> [...]):将文件复制到 <dir> 目的地。它提供了过滤、设置权限、跟随符号链接链等选项。 68 | 69 | \item 70 | file(COPY\_FILE <file> <destination> [...]):将单个文件复制到 <destination> 路径。自 CMake 3.21 开始可用。 71 | 72 | \item 73 | file(SIZE <filename> <out>):读取 <filename> 的大小(以字节为单位),并将其存储在 <out> 变量中。 74 | 75 | \item 76 | file(READ\_SYMLINK <linkname> <out>):读取 <linkname> 符号链接的目标路径,并将其存储在 <out> 变量中。 77 | 78 | \item 79 | file(CREATE\_LINK <original> <linkname> [...]):在 <linkname> 创建指向 <original> 的符号链接。 80 | 81 | \item 82 | file(\{CHMOD|CHMOD\_RECURSE\} <files>... <directories>... PERMISSIONS <permissions>... [...]):设置文件和目录的权限。 83 | 84 | \item 85 | file(GET\_RUNTIME\_DEPENDENCIES [...]):收集各种类型文件的运行时依赖项:可执行文件、库和模块。与 install(RUNTIME\_DEPENDENCY\_SET) 结合使用。 86 | \end{itemize} 87 | 88 | \mySubsubsection{A.4.4.}{路径转换} 89 | 90 | 以下是可用的模式: 91 | 92 | \begin{itemize} 93 | \item 94 | file(REAL\_PATH <path> <out> [BASE\_DIRECTORY <dir>]):从相对路径计算绝对路径,并将其存储在 <out> 变量中。可选地接受 <dir> 基础目录。自 CMake 3.19 开始可用。 95 | 96 | \item 97 | file(RELATIVE\_PATH <out> <directory> <file>):计算 <file> 相对于 <directory> 的路径,并将其存储在 <out> 变量中。 98 | 99 | \item 100 | file(\{TO\_CMAKE\_PATH | TO\_NATIVE\_PATH\} <path> <out>):将 <path> 转换为 CMake 路径(目录以正斜杠分隔)到平台原生路径及反之。结果存储在 <out> 变量中。 101 | \end{itemize} 102 | 103 | \mySubsubsection{A.4.5.}{传输} 104 | 105 | 以下是可用的模式: 106 | 107 | \begin{itemize} 108 | \item 109 | file(DOWNLOAD <url> [<path>] [...]):从 <url> 下载文件,并将其存储在 <path> 中。 110 | 111 | \item 112 | file(UPLOAD <file> <url> [...]):将 <file> 上传到 URL。 113 | \end{itemize} 114 | 115 | \mySubsubsection{A.4.6.}{锁定} 116 | 117 | 锁定模式在 <path> 资源上放置了一个锁 118 | 119 | \begin{shell} 120 | file(LOCK <path> [DIRECTORY] [RELEASE] 121 | [GUARD <FUNCTION|FILE|PROCESS>] 122 | [RESULT_VARIABLE <out>] [TIMEOUT <seconds>] 123 | ) 124 | \end{shell} 125 | 126 | 此锁可选地限定在 FUNCTION、FILE 或 PROCESS,并且可以用 <seconds> 的超时时间限制。为了释放锁,提供 RELEASE 关键词。结果将存储在 <out> 变量中。 127 | 128 | \mySubsubsection{A.4.7.}{归档} 129 | 130 | 归档创建提供以下签名: 131 | 132 | \begin{shell} 133 | file(ARCHIVE_CREATE OUTPUT <destination> PATHS <source>... 134 | [FORMAT <format>] 135 | [COMPRESSION <type> [COMPRESSION_LEVEL <level>]] 136 | [MTIME <mtime>] [VERBOSE] 137 | ) 138 | \end{shell} 139 | 140 | 它在 <destination> 路径创建一个归档文件,其中包含 <source> 文件之一种支持的格式:7zip、gnutar、pax、paxr、raw 或 zip(默认为 paxr)。如果选定的格式支持压缩级别,则可以提供一个单数字整数 0-9,其中 0 为默认值。 141 | 142 | 提取模式具有以下签名: 143 | 144 | \begin{shell} 145 | file(ARCHIVE_EXTRACT INPUT <archive> [DESTINATION <dir>] 146 | [PATTERNS <patterns>...] [LIST_ONLY] [VERBOSE] 147 | ) 148 | \end{shell} 149 | 150 | 从 <archive> 中提取匹配可选 <patterns> 值的文件到目的地 <dir>。如果提供了 LIST\_ONLY 关键词,则文件不会提取,而只会列出。 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /book/content/chapter17/5.tex: -------------------------------------------------------------------------------- 1 | CMake 还支持一些简单的算术运算。请参阅在线文档获取完整详情: 2 | 3 | \url{https://cmake.org/cmake/help/latest/command/math.html} 4 | 5 | 要计算一个数学表达式并将结果以字符串形式存储在 <out> 变量中,可选地使用 <format>(HEXADECIMAL 或 DECIMAL)格式化输出,可以使用以下签名: 6 | 7 | \begin{cmake} 8 | math(EXPR <out> "<expression>" [OUTPUT_FORMAT <format>]) 9 | \end{cmake} 10 | 11 | <expression> 的值是一个字符串,支持 C 代码中存在的运算符(它们在这里具有相同的意义): 12 | 13 | \begin{itemize} 14 | \item 15 | 算术运算符: +, -, *, / 和 \% 模运算 16 | 17 | \item 18 | 位运算符: | 或运算, \& 与运算, \^{} 异或运算, $\textasciitilde$ 非运算, <{}< 左移 和 >{}> 右移 19 | 20 | \item 21 | 括号 (...) 22 | \end{itemize} 23 | 24 | 常数值可以以十进制或十六进制格式提供。 25 | 26 | 27 | -------------------------------------------------------------------------------- /book/content/chapter2/0.tex: -------------------------------------------------------------------------------- 1 | 在CMake语言中编写代码比预期的要棘手。当你第一次阅读CMake列表文件时,可能会觉得里面的语言如此简单,以至于可以不需要理论就能实践。然后可能会试图进行更改并尝试代码,而没有彻底了解它实际上是如何工作的。开发者通常都很忙,而且与构建相关的问题通常不是那种让人愿意投入大量时间的事情。为了快速完成,我们倾向于基于直觉进行更改,希望它们可能会奏效。这种解决技术问题的方法称为\textbf{巫毒编程}。 2 | 3 | CMake语言看起来微不足道:在我们引入了扩展、修复、hack或单行代码之后,突然发现有些东西不工作了。通常,调试的时间会超过理解本身所需的时间。幸运的是,这不会是我们的命运,因为这一章包含了实践中使用CMake语言所需的大部分知识。 4 | 5 | 本章中,不仅会学习CMake语言的基本构建块——注释、命令、变量和控制结构——还将了解必要的背景知识,并遵循最新实践尝试示例。 6 | 7 | CMake会让你处于一个独特的位置。一方面,扮演着构建工程师的角色,必须全面掌握编译器、平台及其所有相关方面。另一方面,是一个编写生成构建系统的代码的开发者。编写高质量的代码是一项具有挑战性的任务,需要多方面的方法。代码不仅要有功能性和可读性,还应该易于分析、扩展和维护。 8 | 9 | 最后,将介绍CMake中最实用和最常用的命令。也经常使用,但程度不如前面的命令将在附录“其他命令”(字符串、列表、文件和数学命令的参考指南)中介绍。 10 | 11 | 本章,将包括以下内容: 12 | 13 | \begin{itemize} 14 | \item 15 | 语法基础 16 | 17 | \item 18 | 变量操作 19 | 20 | \item 21 | 使用列表 22 | 23 | \item 24 | 控制结构 25 | 26 | \item 27 | 常用命令 28 | \end{itemize} 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /book/content/chapter2/1.tex: -------------------------------------------------------------------------------- 1 | 可以在GitHub上找到本章中出现的代码文件,地址为 \url{https://github.com/PacktPublishing/Modern-CMake-for-Cpp-2E/tree/main/examples/ch02}。 2 | 3 | 为了构建本书提供的示例,请使用推荐的命令: 4 | 5 | \begin{shell} 6 | cmake -B <build tree> -S <source tree> 7 | cmake --build <build tree> 8 | \end{shell} 9 | 10 | 请确保将占位符 <build tree> 和 <source tree> 替换为适当的路径。作为提醒:<build tree>是目标/输出目录的路径,而<source tree>是源码所在的位置。 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /book/content/chapter2/4.tex: -------------------------------------------------------------------------------- 1 | 2 | 为了存储一个列表,CMake会将所有元素连接成一个字符串,使用分号(;)作为分隔符:a;list;of;5;elements。可以在元素中使用反斜杠来转义分号:a\verb|\|;single\verb|\|;element。 3 | 4 | 5 | 要创建一个列表,我们可以使用set()命令: 6 | 7 | \begin{cmake} 8 | set(myList a list of five elements) 9 | \end{cmake} 10 | 11 | 由于列表的存储方式,以下命令将产生完全相同的效果: 12 | 13 | \begin{cmake} 14 | set(myList "a;list;of;five;elements") 15 | set(myList a list "of;five;elements") 16 | \end{cmake} 17 | 18 | CMake会自动在未加引号的参数中解包列表。通过传递未加引号的myList引用,我们实际上向命令发送了更多的参数: 19 | 20 | \begin{cmake} 21 | message("the list is:" ${myList}) 22 | \end{cmake} 23 | 24 | message()命令将接收六个参数:“the list is:”, “a”, “list”, “of”, “five” 和 “elements”。这可能会产生意想不到的后果,因为输出将不带额外的空格打印参数: 25 | 26 | \begin{shell} 27 | the list is:alistoffiveelements 28 | \end{shell} 29 | 30 | 这是一个非常简单的机制,应该小心使用。 31 | 32 | CMake提供了一个list()命令,该命令提供了多种子命令来读取、搜索、修改和排序列表。以下是一个简短的摘要: 33 | 34 | \begin{cmake} 35 | list(LENGTH <list> <out-var>) 36 | list(GET <list> <element index> [<index> ...] <out-var>) 37 | list(JOIN <list> <glue> <out-var>) 38 | list(SUBLIST <list> <begin> <length> <out-var>) 39 | list(FIND <list> <value> <out-var>) 40 | list(APPEND <list> [<element>...]) 41 | list(FILTER <list> {INCLUDE | EXCLUDE} REGEX <regex>) 42 | list(INSERT <list> <index> [<element>...]) 43 | list(POP_BACK <list> [<out-var>...]) 44 | list(POP_FRONT <list> [<out-var>...]) 45 | list(PREPEND <list> [<element>...]) 46 | list(REMOVE_ITEM <list> <value>...) 47 | list(REMOVE_AT <list> <index>...) 48 | list(REMOVE_DUPLICATES <list>) 49 | list(TRANSFORM <list> <ACTION> [...]) 50 | list(REVERSE <list>) 51 | list(SORT <list> [...]) 52 | \end{cmake} 53 | 54 | 大多数时候,我们的项目中并不真正需要使用列表。然而,如果发现自己处于那种罕见的情况,这个概念会很方便,可以在附录中找到list()命令的更深入的参考资料。 55 | 56 | 现在,知道了如何处理各种类型的列表和变量,让我们将焦点转移到控制执行流程上,并了解CMake中可用的控制结构。 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /book/content/chapter2/6.tex: -------------------------------------------------------------------------------- 1 | 2 | CMake 提供了许多脚本命令,允许处理变量和环境。其中一些在附录中已经进行了详细介绍,例如 :list()、string() 和 file()。其他,如: find\_file()、find\_package() 和 find\_path(),更适合放在属于它们主题的章节中介绍。 3 | 4 | 本节中,我们将简述一些常用命令: 5 | 6 | \begin{itemize} 7 | \item 8 | message() 9 | 10 | \item 11 | include() 12 | 13 | \item 14 | include\_guard() 15 | 16 | \item 17 | file() 18 | 19 | \item 20 | execute\_process() 21 | \end{itemize} 22 | 23 | \mySubsubsection{2.6.1}{message()} 24 | 25 | message() 命令,将文本输出到标准输出,但其功能远不止于此。其有一个 MODE 参数,可以定义命令的行为,如下所示:message(<MODE> "text to print")。 26 | 27 | 可用的MODE如下所示: 28 | 29 | \begin{itemize} 30 | \item 31 | FATAL\_ERROR: 停止处理和生成。 32 | 33 | \item 34 | SEND\_ERROR: 继续处理,但跳过生成。 35 | 36 | \item 37 | WARNING: 继续处理。 38 | 39 | \item 40 | AUTHOR\_WARNING: 输出警告,但继续处理。 41 | 42 | \item 43 | DEPRECATION: 如果启用了 CMAKE\_ERROR\_DEPRECATED 或 CMAKE\_WARN\_DEPRECATED,则输出相应地信息。 44 | 45 | \item 46 | NOTICE 或省略模式(默认): 输出消息到 stderr,以吸引使用者的注意。 47 | 48 | \item 49 | STATUS: 继续处理,推荐用于向用户显示的主要消息。 50 | 51 | \item 52 | VERBOSE: 继续处理,应用于更详细的信息,通常不是非常必要。 53 | 54 | \item 55 | DEBUG: 继续处理,应包含项目出现问题时,对处理问题有帮助的详细信息。 56 | 57 | \item 58 | TRACE: 继续处理,建议在项目开发期间输出消息。通常,这类消息会在发布项目之前移除。 59 | \end{itemize} 60 | 61 | 选择正确的模式需要花点心思,可以通过基于严重性(自 3.21 版本起)给输出文本着色来节省调试时间,甚至在声明不可恢复的错误后停止执行: 62 | 63 | \filename{ch02/10-useful/message\_error.cmake} 64 | 65 | \begin{cmake} 66 | message(FATAL_ERROR "Stop processing") 67 | message("This won't be printed.") 68 | \end{cmake} 69 | 70 | 消息将根据当前的日志级别(默认为 STATUS)打印,在上一章的调试和跟踪选项部分讨论了如何更改此设置。 71 | 72 | 在第 1 章中,我提到了使用 CMAKE\_MESSAGE\_CONTEXT 进行调试,现在是深入研究它的时候了。在此期间,我们来了解了这个主题三个关键部分:列表、作用域和函数。 73 | 74 | 在复杂的调试场景中,指出消息发生的上下文会非常有用。考虑以下输出,其中在 foo 函数中打印的消息有适当的前缀: 75 | 76 | \begin{shell} 77 | $ cmake -P message_context.cmake --log-context 78 | [top] Before `foo` 79 | [top.foo] foo message 80 | [top] After `foo` 81 | \end{shell} 82 | 83 | 具体代码: 84 | 85 | \filename{ch02/10-useful/message\_context.cmake} 86 | 87 | \begin{cmake} 88 | function(foo) 89 | list(APPEND CMAKE_MESSAGE_CONTEXT "foo") 90 | message("foo message") 91 | endfunction() 92 | 93 | list(APPEND CMAKE_MESSAGE_CONTEXT "top") 94 | message("Before `foo`") 95 | foo() 96 | message("After `foo`") 97 | \end{cmake} 98 | 99 | 来分解一下: 100 | 101 | \begin{enumerate} 102 | \item 103 | 首先,将 top 追加到上下文跟踪变量 CMAKE\_MESSAGE\_CONTEXT,然后打印最初的“Before 'foo' ”,匹配的前缀 [top] 将添加到输出中。 104 | 105 | \item 106 | 接下来,进入 foo() 函数时,我们在属于该函数的列表后,追加一个名为 foo 的新上下文,并输出另一个消息,该消息在输出中显示扩展的 [top.foo] 前缀。 107 | 108 | \item 109 | 最后,在函数执行完成后,我们打印“After 'foo'”。消息以原始的 [foo] 作用域打印。为什么?因为变量作用域规则:更改的 CMAKE\_MESSAGE\_CONTEXT 变量只在函数作用域结束前有效,然后恢复为原始未更改的版本 110 | \end{enumerate} 111 | 112 | message() 的另一个技巧是向 CMAKE\_MESSAGE\_INDENT 列表添加缩进(与 CMAKE\_MESSAGE\_CONTEXT 完全相同的方式): 113 | 114 | \begin{cmake} 115 | list(APPEND CMAKE_MESSAGE_INDENT " ") 116 | message("Before `foo`") 117 | foo() 118 | message("After `foo`") 119 | \end{cmake} 120 | 121 | 然后,脚本输出看起来更简单: 122 | 123 | \begin{shell} 124 | Before `foo` 125 | foo message 126 | After `foo` 127 | \end{shell} 128 | 129 | 由于CMake没有提供断点或其他调试工具,当事情没有按计划进行时,清晰的日志信息就显得尤为重要。 130 | 131 | \mySubsubsection{2.6.2}{include()} 132 | 133 | 将代码分割成不同的文件,以保持有序和分离。然后,可以通过 include() 从父列表文件引用它们: 134 | 135 | \begin{shell} 136 | include(<file|module> [OPTIONAL] [RESULT_VARIABLE <var>]) 137 | \end{shell} 138 | 139 | 如果提供一个文件名(带有 .cmake 扩展名的路径),CMake 将尝试打开并执行。 140 | 141 | 注意,这也不会创建独立的变量作用域,因此该文件中对变量的修改都会对调用作用域产生影响。 142 | 143 | 如果文件不存在,CMake 将报错,除非使用 OPTIONAL 关键字指定相应文件为“可选的”。当需要知道 include() 是否成功时,可以提供 RESULT\_VARIABLE 关键字以及变量的名称。在成功时,它的内容为文件的完整路径;在失败时,则为 NOTFOUND。 144 | 145 | 使用脚本模式下运行时,相对路径都将以当前工作目录解析相对路径。要强制相对于脚本本身进行搜索,请提供绝对路径: 146 | 147 | \begin{cmake} 148 | include("${CMAKE_CURRENT_LIST_DIR}/<filename>.cmake") 149 | \end{cmake} 150 | 151 | 如果不提供路径,但提供了模块的名称(不带 .cmake 或其他),CMake 将尝试找到一个模块并包含它。CMake 将在 CMAKE\_MODULE\_PATH 中搜索名为 <模块>.cmake 的文件,然后是在 CMake 模块目录中搜索。 152 | 153 | 当 CMake 遍历源树,并包含了不同的列表文件时,将设置以下变量: 154 | 155 | \begin{itemize} 156 | \item 157 | CMAKE\_CURRENT\_LIST\_DIR 158 | 159 | \item 160 | CMAKE\_CURRENT\_LIST\_FILE 161 | 162 | \item 163 | CMAKE\_PARENT\_LIST\_FILE 164 | 165 | \item 166 | CMAKE\_CURRENT\_LIST\_LINE 167 | \end{itemize} 168 | 169 | \mySubsubsection{2.6.3}{include\_guard()} 170 | 171 | 对于有些文件,我们只希望包含一次,这时 include\_guard([DIRECTORY|GLOBAL]) 就可以使用了。 172 | 173 | 将 include\_guard() 放在包含文件的顶部。当 CMake 第一次遇到它时,将在当前作用域中进行记录。如果文件再次包含,CMake就不会再对该文件进行处理了。 174 | 175 | 避免在隔离相关的作用域,应该提供 DIRECTORY 或 GLOBAL 参数。顾名思义,DIRECTORY 关键字将在当前目录及其子目录应用保护,而 GLOBAL 关键字将把保护应用于整个构建。 176 | 177 | 178 | \mySubsubsection{2.6.4}{file()} 179 | 180 | 为了了解可以在 CMake 脚本中执行的操作,来了解一下文件操作命令: 181 | 182 | \begin{shell} 183 | file(READ <filename> <out-var> [...]) 184 | file({WRITE | APPEND} <filename> <content>...) 185 | file(DOWNLOAD <url> [<file>] [...]) 186 | \end{shell} 187 | 188 | 简而言之,file() 命令可以以系统无关的方式读取、写入和传输文件,以及与文件系统、文件锁、路径和存档等交互。有关更多详细信息,请参阅附录。 189 | 190 | \mySubsubsection{2.6.5}{execute\_process()} 191 | 192 | 有时候,需要使用系统中的工具(毕竟,CMake 主要是一个构建系统生成器)。CMake 提供了一个命令以实现此目的:可以使用 execute\_process() 来运行其他进程,并收集输出。这个命令非常适合脚本使用,也可以在项目中使用,但它仅在配置阶段有效。 193 | 194 | 以下是该命令的一般形式: 195 | 196 | \begin{shell} 197 | execute_process(COMMAND <cmd1> [<arguments>]... [OPTIONS]) 198 | \end{shell} 199 | 200 | CMake 将使用操作系统的 API 来创建一个子进程(因此,像 \&\&, || 和 > 这样的 shell 操作符将不起作用),可以通过多次提供 COMMAND 参数来链接命令,并将一个命令的输出传递给另一个。 201 | 202 | 可选地 TIMEOUT 参数,用来在进程未在所需限制内完成任务时终止该进程,并且可以根据需要设置 WORKING\_DIRECTORY 。 203 | 204 | 所有任务的退出代码可以通过提供 RESULTS\_VARIABLE 参数来收集到一个列表中。如果只对最后执行的命令的结果感兴趣,请使用单数形式:RESULT\_VARIABLE 。 205 | 206 | 为了收集输出,CMake 提供了两个参数:OUTPUT\_VARIABLE 和 ERROR\_VARIABLE(用法类似)。如果想合并 stdout 和 stderr,请为这两个参数使用相同的变量。 207 | 208 | 当为其他用户编写项目时,应该确保计划使用的命令,在声明支持的平台上可用。 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /book/content/chapter2/7.tex: -------------------------------------------------------------------------------- 1 | 本章打开了使用 CMake 编程的大门 —— 现在能够编写出色、富有信息量的注释,并使用内置命令,了解了如何正确地提供各种类型的参数。仅凭这些知识,就能理解 CMake 列表文件的不寻常语法。我们讨论了 CMake 中的变量 —— 特别是如何引用、设置和取消设置普通变量、缓存变量和环境变量。还深入探讨了文件和目录变量作用域的工作原理,如何创建它们,以及可能遇到的问题和问题的解决办法。我们还介绍了列表和控制结构,检查了条件的语法、逻辑操作、未引用参数的计算,以及字符串和变量。还学习了如何比较值、进行简单的检查,以及检查系统中文件的状态。这样,就能够编写条件块和 while 循环;在讨论循环时,我们还介绍了 foreach 循环的语法。 2 | 3 | 理解如何使用宏和函数语句,自定义命令无疑将促进编写更清晰、更有条理的代码。我们还讨论了改进代码结构和创建更具可读性名称的策略。 4 | 5 | 最后,介绍了 message() 命令及其多个日志级别。还研究了如何划分和包含列表文件,并且发现了一些有趣的命令。 6 | 7 | 全部的这些,为迎接下一章做好了充分的准备。 8 | -------------------------------------------------------------------------------- /book/content/chapter2/8.tex: -------------------------------------------------------------------------------- 1 | 2 | \begin{itemize} 3 | \item 4 | 《干净的代码:敏捷软件工艺之道》(作者:Robert C. Martin): 5 | 6 | \url{https://amzn.to/3cm69DD} 7 | 8 | \item 9 | 《重构:改善既有代码的设计》(作者:Martin Fowler): 10 | 11 | \url{https://amzn.to/3cmWk8o} 12 | 13 | \item 14 | 哪些是好的代码注释?(Rafał Świdziński): 15 | 16 | \url{https://youtu.be/4t9bpo0THb8} 17 | 18 | \item 19 | CMake中设置和使用变量的语法是什么?(StackOverflow): 20 | 21 | \url{https://stackoverflow.com/questions/31037882/whats-the-cmake-syntax-toset-and-use-variables} 22 | \end{itemize} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /book/content/chapter2/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter2/images/1.png -------------------------------------------------------------------------------- /book/content/chapter2/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter2/images/2.png -------------------------------------------------------------------------------- /book/content/chapter2/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter2/images/3.png -------------------------------------------------------------------------------- /book/content/chapter3/0.tex: -------------------------------------------------------------------------------- 1 | 编程既是一门艺术,也是一种深奥的技术过程,二者都非常困难。因此,应尽可能优化这个过程。我们很少能通过简单地切换开关来获得更好的结果,但使用集成开发环境(IDE)无疑是一种好方法。 2 | 3 | 如果以前没有使用过合适的IDE(或者认为像Emacs或Vim这样的文本处理器已经是最优选择),这一章节就是为您准备的。如果您是一位经验丰富的专业人士,并且已经对这一主题很熟悉,可以把这一章节作为当前最佳选择的快速概览,或许可以考虑转换,或者确认当前的工具就是最好的。 4 | 5 | 本章着重于为初入该领域的人提供易入门的介绍,对IDE的关键选择提供了一个温和的入门。我们将讨论为什么需要IDE,以及如何选择最适合需求的IDE。当然,市场上有很多选择,如果选择正确,有许多因素可能会影响您的生产力。我们将讨论一些在特定规模的组织中工作可能重要的考虑因素,确保能够把握这些细微之处,而不会陷入复杂性的漩涡。然后,我们将简要介绍工具链,讨论可用的选择。 6 | 7 | 然后,将介绍几种流行IDE的独特品质,如先进的CLion,适应性强的Visual Studio Code,以及功能强大的Visual Studio IDE。每个部分都经过精心设计,以展示这些IDE的优势和高端功能。 8 | 9 | 本章中,将包含以下内容: 10 | 11 | \begin{itemize} 12 | \item 13 | 了解IDE 14 | 15 | \item 16 | CLion IDE 17 | 18 | \item 19 | Visual Studio Code 20 | 21 | \item 22 | Visual Studio IDE 23 | \end{itemize} 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /book/content/chapter3/1.tex: -------------------------------------------------------------------------------- 1 | 本节中,我们将讨论集成开发环境(IDE),以及它们如何显著提高开发速度和代码质量。让我们了解IDE是什么开始吧。 2 | 3 | 为什么使用IDE,以及如何选择IDE?IDE,或集成开发环境,是一种全面的工具,将各种专用工具结合在一起,简化了软件开发过程。创建专业项目的过程涉及许多步骤:设计、编码、构建、测试、打包、发布和维护。每个步骤都包含许多较小的任务,复杂性可能会让人不堪重负。IDE通过提供一个平台来解决这一问题,该平台配备了一套由IDE创建者策划和配置的工具。这种集成能够无缝地使用这些工具,而无需为每个项目进行单独设置。 4 | 5 | IDE主要围绕代码编辑器、编译器和调试器展开。旨在提供足够的集成,能够编辑代码,立即编译,并附加调试器运行。IDE可以包含构建工具链,或者允许开发者选择自己喜欢的编译器和调试器。编辑器通常是软件的核心部分,可以通过插件进行大量扩展,如代码高亮、格式化等。 6 | 7 | 更高级的IDE提供了非常复杂的功能,如热重载调试(在Visual Studio 2022中可用;继续阅读以了解更多信息)。此功能允许在调试器中运行代码,编辑它,并在不重新启动程序的情况下继续执行。还会发现重构工具,用于重命名符号或将代码提取到单独的函数中,以及静态分析,用于在编译前识别错误。此外,IDE提供了使用Git和其他版本控制系统的工具,这对于解决冲突来说是无价的。 8 | 9 | 可以看到,早期学习如何使用IDE,并在组织中标准化这种使用是多么有益。来看看为什么选择合适的IDE很重要。 10 | 11 | \mySubsubsection{3.1.1}{选择IDE} 12 | 13 | 社区公认为功能齐全的IDE有几个。致力于特定选择之前,建议先研究一下这个领域,特别是当前软件发布周期的速度和该领域的变化都非常快。 14 | 15 | 在我几年的企业经验中,很少有一个IDE提供的功能足够吸引人,使得有人从一种IDE切换到另一种IDE。习惯的力量对开发者来说是第二天性,不应忽视。记住,当在IDE中感到舒适,它很可能会成为未来相当长一段时间的首选工具。这就是为什么仍然看到开发者使用Vim(1991年发布的基于控制台的文本编辑器),通过一堆插件使其像更现代的、基于GUI的IDE一样强大。 16 | 17 | 开发者选择一个IDE,而不是另一个的原因有很多;其中一些非常重要(速度、可靠性、全面性、完整性),而其他的……则不那么重要。我想分享我对这个选择的个人观点,希望这会对阅读本书的人有用。 18 | 19 | \mySamllsection{选择全面的IDE} 20 | 21 | 刚开始,可能会考虑使用简单的文本编辑器,并运行几个命令来构建代码。这种方法可行,尤其是试图理解基础知识时。这也会帮助理解初学者在没有IDE的情况下,可能会有的体验。 22 | 23 | 另一方面,IDE是有目创造的,简化了开发人员在项目生命周期中处理的许多过程。尽管最初可能会让人感到不知所措,但请选择一个包含必要功能的全面IDE。确保它尽可能完整,但也要注意成本,因为对于小型企业或个人开发者来说,IDE可能会很昂贵。这是在手动管理上花费的时间与IDE提供的功能成本之间的平衡。 24 | 25 | 无论成本如何,始终选择具有强大社区支持的IDE,以便在遇到问题时提供帮助。探索社区论坛和像StackOverflow.com这样的热门问答网站,检查用户是否得到了他们的答案。此外,选择一个由知名公司积极开发的IDE。没人想浪费时间在长时间没有更新,可能会在不久的将来弃用或放弃的东西上。例如,GitHub创建的编辑器Atom在发布了7年后淘汰。 26 | 27 | \mySamllsection{选择组织中广泛支持的IDE} 28 | 29 | 反直觉的是,这可能与每位开发者的偏好不一致。您可能已经对大学、上一份工作或个人项目中的不同工具有所熟悉。如前所述,这种习惯可能会让您忽略公司的建议,坚持使用您所知道的工具。需要抵制这种诱惑,随着时间的推移,这样的选择会变得越来越具挑战性。 30 | 31 | 我分别在爱立信、亚马逊和思科工作过,只有一次尝试配置和维护非标准IDE的努力是值得的。因为我设法获得了足够的组织支持来集体解决问题,但阅读本书各位的主要目标应该是编写代码,而不是与不受支持的IDE作斗争。学习推荐的软件可能需要付出努力,但这比违背常规所需的努力要少(是的,Vim在这场战斗中失败了;是时候继续前进了)。 32 | 33 | \mySamllsection{不要根据目标操作系统和平台选择IDE} 34 | 35 | 如果您正在为Linux开发软件,需要使用Linux机器和基于Linux的IDE。然而,C++是一种可移植的语言,所以只要正确编写了代码,就应该在任何平台上以相同的方式编译和运行。当然,可能会在库上遇到问题,因为并非所有库都会默认安装,有些可能特定于平台。 36 | 37 | 严格遵循目标平台并不是必要的,有时甚至可能适得其反。例如,针对的是较旧或长期支持(LTS)版本的操作系统,可能无法使用最新的工具链版本。如果希望在不同于目标平台的平台上开发,可以这样做。 38 | 39 | 考虑交叉编译或远程开发。交叉编译涉及使用专用工具链,允许在一种平台(如Windows)上运行的编译器为另一种平台(如Linux)生成工件。这种方法在业界得到了广泛使用,并且得到了CMake的支持。另外,建议使用远程开发,将代码发送到目标机器,并在那里使用本地工具链进行构建。这种方法得到了许多IDE的支持,我们将在下一节中进行探讨。 40 | 41 | \mySamllsection{选择支持远程开发的IDE} 42 | 43 | 虽然这不应该是首要标准,但在满足其他要求后,考虑IDE中的远程开发支持是有益的。随着时间的推移,即使是经验丰富的开发者也会遇到由于团队、项目甚至公司的变化而需要不同于他们常用操作系统的目标平台的项目。 44 | 45 | 如果首选IDE支持远程开发,可以继续使用它,利用在不同操作系统上编译和调试代码的能力,并在IDE的GUI中查看结果。远程开发相对于交叉编译的主要优势在于其集成的调试器支持,无需CMake项目级别的配置,过程更加简洁。此外,公司通常会提供强大的远程机器,允许开发者使用更便宜、更轻便的本地设备。 46 | 47 | 当然,有人可能会提出交叉编译提供了对开发环境的更大控制权,允许为测试进行临时更改。它不需要代码传输的带宽,支持低端的互联网连接或离线工作。然而,考虑到大多数软件开发都涉及互联网访问以获取信息,这可能是一个不那么关键的优势。使用像Docker这样的虚拟化环境可以运行本地生产副本并设置远程开发连接,提供安全性、可定制性和构建及部署容器的能力。 48 | 49 | 这里提到的考虑因素稍微倾向于在大公司工作的情况,在那里事情进展较慢,很难进行具有重大影响的改变。这些建议并不否定您决定根据需要优先考虑IDE的其他方面时,与CMake一起拥有完美完整体验的可能性。 50 | 51 | \mySubsubsection{3.1.2}{安装工具链} 52 | 53 | IDE集成了所有必要的工具来简化软件开发。这一过程的关键部分是构建二进制文件,有时在后台或即时进行,以为开发者提供额外信息。工具链是一系列工具的集合,如编译器、链接器、归档器、优化器、调试器,以及标准C++库的实现。还包括其他有用的实用程序,如bash、make、gawk、grep等,用于构建程序。 54 | 55 | 一些IDE自带工具链或工具链下载器,而其他则没有。最好的做法是运行已安装的IDE,并检查是否能够编译基本的测试程序。CMake通常在配置阶段默认执行此操作,大多数IDE将此作为新项目初始化的一部分。如果此过程失败,IDE或操作系统的包管理器可能会提示安装必要的工具。只需按照流程操作,因为这种流程通常准备得很充分。 56 | 57 | 如果没有提示,或者想使用特定的工具链,以下是一些基于平台的选项: 58 | 59 | \begin{itemize} 60 | \item 61 | GCC (\url{https://gcc.gnu.org/}) 用于Linux、Windows(通过MinGW或Cygwin)、macOS以及许多其他平台。GCC是最受欢迎和广泛使用的C++编译器,支持大多数的平台和架构。 62 | 63 | \item 64 | Clang/LLVM (\url{https://clang.llvm.org/}) 用于Linux、Windows、macOS以及许多其他平台。Clang是C、C++和Objective-C编程语言的前端编译器,使用LLVM作为其后端。 65 | 66 | \item 67 | Microsoft Visual Studio/MSVC (\url{https://visualstudio.microsoft.com/}) 主要用于Windows,通过Visual Studio Code和CMake提供跨平台支持。MSVC是Microsoft提供的C++编译器,通常在Visual Studio IDE内使用。 68 | 69 | \item 70 | MinGW-w64 (\url{http://mingw-w64.org/}) 用于Windows。MinGW-w64是原始MinGW项目的进一步发展,旨在为64位Windows和新API提供更好的支持。 71 | 72 | \item 73 | Apple Clang (\url{https://developer.apple.com/xcode/cpp/}) 用于macOS、iOS、iPadOS、watchOS和tvOS。Apple的Clang版本,针对Apple的硬件和软件生态系统进行了优化,与Xcode集成。 74 | 75 | \item 76 | Cygwin (\url{https://www.cygwin.com/}) 用于Windows。Cygwin在Windows上提供了一个POSIX兼容的环境,允许使用GCC和其他GNU工具。 77 | \end{itemize} 78 | 79 | 如果想快速开始,而不深入研究每个工具链,可以参考我的个人偏好:如果IDE没有提供工具链,Windows上使用MinGW,Linux上使用Clang/LLVM,macOS上使用Apple Clang。这些工具链都很好地适用于它们的主要平台,并且通常提供最佳体验。 80 | 81 | \mySubsubsection{3.1.3}{使用本书的示例与IDE} 82 | 83 | 本书附带了一系列CMake项目的示例,可在官方GitHub仓库中找到:\url{https://github.com/PacktPublishing/Modern-CMake-for-Cpp-2E}。 84 | 85 | 当然,探讨IDE的主题时,如何使用这个仓库,以及这里展示的所有IDE?好吧,我们需要认识到,教你如何创建专业项目这本书本身并不是一个专业项目。它是一系列这样的项目,完成程度各不相同,可能在合理的情况下进行了简化。不幸的是(或者也许幸运的是?),IDE并不是为了加载数十个项目,并方便地管理它们而构建的。它们通常将功能集中在加载一个正在编辑的项目上。 86 | 87 | 这让我们处于一个有些尴尬的位置:使用IDE导航示例集真的很难。当使用IDE加载示例集时,通过选择示例目录来打开它,大多数IDE会检测到多个CMakeLists.txt文件并要求选择一个。这样做之后,通常的初始化过程会发生,会写入临时文件,运行CMake配置和生成阶段,以使项目进入可以构建的状态。大多数IDE确实提供了在工作空间中切换不同目录(或项目)的方法,但可能并不像我们希望的那样直接。 88 | 89 | 如果您在这方面遇到困难,有两个选择:要么不使用IDE构建示例(而是使用控制台命令),要么每次将一个示例加载到新项目中。如果热衷于练习命令,我会推荐第一个选项,因为这些命令将来可能会派上用场,并且会让您更好地理解幕后发生的事情。这对于构建工程师来说通常是一个不错的选择,因为这种知识会经常使用。另一方面,如果主要作为开发者专注于代码的业务方面,那么早期使用IDE可能是最好的选择。 90 | 91 | 有了这些,让我们专注于回顾当今顶尖的IDE,看看哪个可能最适合您。 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /book/content/chapter3/2.tex: -------------------------------------------------------------------------------- 1 | 2 | CLion是一款付费的跨平台IDE,适用于Windows、macOS和Linux,由JetBrains开发。是的,这款软件是基于订阅的;截至2024年初,可以以99.00美元的价格获得一年的个人使用许可。更大的组织支付更多,初创企业支付更少。如果是学生或发布开源项目,可以获得免费许可。此外,还有30天的试用期来测试软件。这是列表中唯一一款不提供免费“社区”或精简版的IDE。无论如何,这是一款由知名公司开发的坚实软件。 3 | 4 | 图3.1显示了CLion IDE在浅色模式(深色模式是默认选项)的外观: 5 | 6 | \myGraphic{1.0}{content/chapter3/images/1.png}{图3.1:CLion IDE的主窗口} 7 | 8 | 这是一个功能齐全的IDE,准备好应对可能抛给它的任何事情。来看看它如何脱颖而出的吧! 9 | 10 | \mySubsubsection{3.2.1}{可能喜欢它的原因} 11 | 12 | 与替代方案不同,CLion支持C和C++,这是它支持的第一种和唯一一种语言。这个IDE的许多功能都是专门为支持这个环境而设计的,并与C/C++思维方式保持一致。当比较其他IDE的功能时,这一点非常明显:代码分析、代码导航、集成的调试器和重构工具,可以在像Visual Studio IDE这样的竞品中找到。然而,它们并不是那么深入和强大地面向C/C++。 13 | 14 | 无论如何,CMake可以完全集成到CLion中,是这个IDE中的首选项目格式。然而,Autotools和Makefile项目处于早期支持状态,可以用来最终迁移到CMake。CLion原生支持CMake的CTest,并与许多单元测试框架集成,还有专门的流程来生成代码、运行测试和收集展示结果。可以使用Google Test、Catch、Boost.Test和doctest。 15 | 16 | 我特别喜欢的一个功能是,能够使用Docker在容器中开发C++程序——稍后会详细介绍。同时,让我们看看如何开始使用CLion。 17 | 18 | \mySubsubsection{3.2.2}{第一步} 19 | 20 | 从官方网站下载CLion(\url{https://www.jetbrains.com/clion})后,可以按照使用的平台进行通常的安装过程。CLion在Windows(图3.2)和macOS(图3.3)上提供了一个合理的视觉安装程序。 21 | 22 | \myGraphic{0.6}{content/chapter3/images/2.png}{图3.2:CLion的Windows安装程序} 23 | 24 | \myGraphic{0.6}{content/chapter3/images/3.png}{图3.3:CLion的macOS安装程序} 25 | 26 | 在Linux上,需要解压下载的存档并运行安装脚本: 27 | 28 | \begin{shell} 29 | tar -xzf CLion-<version>.tar.gz 30 | ./CLion-<version>/bin/CLion.sh 31 | \end{shell} 32 | 33 | 这些说明可能已经过时,所以请务必去CLion官网进行确认。 34 | 35 | 首次运行时,将要求提供许可证代码或开始30天的免费试用期。选择第二个选项将允许试用IDE并确定它是否适合。接下来,将能够创建新项目并选择目标C++版本。之后,CLion将检测可用的编译器和CMake版本,并尝试构建一个测试项目以确认一切设置正确。在某些平台(如macOS)上,可能会自动提示安装所需的开发者工具。在其他平台,可能需要自己设置,并确保它们在PATH环境变量中可用。 36 | 37 | 接下来,确保具链已根据需求进行配置。工具链是按项目配置的,因此创建一个默认的CMake项目。然后,导航到设置/首选项(Ctrl/Command + Alt + S)并选择构建、执行、部署 > CMake。在这个选项卡上,可以配置构建配置文件(图3.3)。可能很有用的是添加一个Release配置文件,来构建没有调试符号的优化工件。要添加一个,只需在上方配置文件列表上按加号按钮。CLion将创建一个默认的Release配置文件。还可以在主窗口顶部下拉菜单中,切换配置文件。 38 | 39 | 现在,可以简单地按F9来编译并运行带有调试器的程序。接下来,阅读CLion的官方文档,因为其中有很多有用的功能可以探索。 40 | 41 | 接下来,我将绍我最喜欢的部分:调试器。 42 | 43 | \mySubsubsection{3.2.3}{高级功能:强大的调试器} 44 | 45 | CLion的调试功能非常先进,特别是为C++量身定制。我非常高兴地发现了一个最新的添加功能——CMake调试,包括许多标准的调试功能:通过代码、断点、观察、内联值探索等。当事情并不像预期那样工作时,能够探索不同作用域(缓存、ENV和当前作用域)中的变量是非常方便的。 46 | 47 | 对于C++调试,您将获得GNU项目调试器(GDB)提供的许多标准功能,例如汇编视图、断点、单步执行、观察点等,但也有重大改进。在CLion中,会发现一个并行堆栈视图,允许以图形化的方式查看所有线程,及其当前堆栈帧。此外,还有一个高级内存视图功能,用于查看运行程序在RAM中的布局,并实时修改内存。CLion还提供了多种其他工具来帮助您了解发生了什么:寄存器视图、代码反汇编、调试器控制台、核心转储调试、可执行文件的调试等。 48 | 49 | 最后,CLion有一个非常出色执行的评估表达式功能,它非常神奇,甚至允许程序执行过程中修改对象。只需右键点击代码行,然后从菜单中选择此功能。 50 | 51 | 这就是关于CLion的全部内容;现在是时候看看另一个IDE了。 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /book/content/chapter3/3.tex: -------------------------------------------------------------------------------- 1 | 2 | Visual Studio Code(VS Code)是一款免费的、跨平台的IDE,适用于Windows、macOS和Linux,由微软开发。不要将它与另一款微软产品混淆,即Visual Studio IDE。 3 | 4 | VS Code因其广泛的扩展生态系统和对数百种编程语言的支持而受到青睐(据估计,超过220种不同的语言!)。微软收购GitHub时,VS Code就传言为Atom的替代。 5 | 6 | IDE的整体设计非常出色,如图3.4所示。 7 | 8 | \myGraphic{0.6}{content/chapter3/images/4.png}{图3.4:VS Code的主窗口} 9 | 10 | 现在,找出VS Code之所以特别的原因。 11 | 12 | \mySubsubsection{3.3.1}{可能喜欢它的原因} 13 | 14 | 在VS Code支持的众多语言中,C++并不是优先考虑的,有了许多高级语言扩展。这种权衡带来的好处是,能够在同一环境中根据需要切换多种语言。 15 | 16 | 这个工具的学习曲线稍微有些陡峭,大多数扩展都符合基本UI功能,而不是自行实现的高级界面。许多功能将通过命令面板(通过按F1访问)提供,这要求输入命令,而非点击图标或按钮。为了保持VS Code干净、快速且免费,这是一个合理的牺牲。实际上,这个IDE加载速度非常快,以至于我更喜欢将它作为通用文本编辑器使用。 17 | 18 | VS Code的真正强大之处在于它拥有大量出色的扩展,其中大多数都免费。对于C++和CMake,有专门的扩展可用,我们将在下一节看看如何配置它们。 19 | 20 | \mySubsubsection{3.3.2}{第一步} 21 | 22 | VS Code可以从官方网站获得:\url{https://code.visualstudio.com/}。该网站提供了Windows和macOS,以及许多Linux发行版(Debian、Ubuntu、Red Hat、Fedora、SUSE)的下载列表。按照平台的常规流程安装软件后,需要通过扩展市场(通过按Ctrl/Command + Shift + X)安装一堆扩展。以下是一些推荐的入门扩展: 23 | 24 | \begin{itemize} 25 | \item 26 | C/C++ by Microsoft 27 | 28 | \item 29 | C/C++ Extension Pack by Microsoft 30 | 31 | \item 32 | CMake by twxs 33 | 34 | \item 35 | CMake Tools by Microsoft 36 | \end{itemize} 37 | 38 | 它们将提供标准的代码高亮和直接从IDE编译、运行和调试代码的能力,但需要自己安装工具链。通常,VS Code会在开始打开相关文件时在弹出窗口中建议安装扩展,所以不必特意去寻找。 39 | 40 | 如果参与远程项目,我建议安装Microsoft的Remote-SSH扩展,因为这会使体验更加连贯和舒适;这个扩展不仅处理文件同步,还允许通过远程机器的调试器远程调试。 41 | 42 | 然而,还有一个更有趣的扩展。 43 | 44 | \mySubsubsection{3.3.3}{高级功能:Dev Containers} 45 | 46 | 如果要将应用程序部署到生产环境,无论是发送编译的工件还是运行构建过程,确保所有依赖项都存在至关重要;否则,将遇到各种问题。即使考虑了所有依赖项,不同的版本或配置也可能导致解决方案与开发或测试环境的行为不同,我多次遇到过这种情况。在虚拟化变得普遍之前,处理环境问题只是生活的一部分。 47 | 48 | 随着轻量级容器如Docker的引入,事情变得更加简单。可以运行一个包含您服务的最小化操作系统,并将其隔离到自己的空间中,并将所有依赖项打包到容器中。 49 | 50 | 直到最近,开发容器涉及手动构建、运行和通过IDE的远程会话连接到容器。这个过程并不太难,但它需要手动步骤,不同开发人员可能会以不同的方式执行。 51 | 52 | 近年来,微软发布了一个名为Dev Containers(\url{https://containers.dev/})的开放标准,该规范主要由一个devcontainer.json文件组成,可以在项目仓库中放置该文件,指导IDE如何在一个容器中设置其开发环境。 53 | 54 | 要使用此功能,只需安装Microsoft的Dev Containers扩展,并将其指向一个适当准备的项目仓库。如果不介意切换主CMakeLists.txt,可以尝试使用本书的仓库: 55 | 56 | \url{git@github.com:PacktPublishing/Modern-CMake-for-Cpp-2E.git} 57 | 58 | 我可以确认,其他IDE,如CLion,也在采用这个标准。如果正面临这种情况,这是一个很好的实践。现在是时候转向微软家族的下一个产品了。 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /book/content/chapter3/4.tex: -------------------------------------------------------------------------------- 1 | 2 | Visual Studio(VS)IDE是微软开发的一款适用于Windows的IDE。VS曾为macOS提供支持,但将在2024年8月弃用,要将其与微软的另一个IDE——VS Code区分开来。 3 | 4 | VS有几个版本:社区版、专业版和企业版。社区版免费,允许一家公司最多有五名用户。规模更大的公司需要支付许可费用,每月每位用户起价45美元。图3.5展示了VS 2022的外观: 5 | 6 | \myGraphic{0.6}{content/chapter3/images/5.png}{图3.5:VS 2022的主窗口} 7 | 8 | 和其他IDE一样,也可以启用暗黑模式。 9 | 10 | \mySubsubsection{3.4.1}{可能喜欢它的原因} 11 | 12 | 这个IDE与VS Code共享许多功能,提供类似但更加精致的体验。套件中充满了功能,其中许多功能利用了图形用户界面、向导和可视元素。大多数这些功能都直接开箱即用,而不是通过扩展(尽管仍然有一个大型且广泛的包市场以获取更多功能)。 13 | 14 | 根据版本的不同,测试工具将涵盖广泛的测试:单元测试、性能测试、负载测试、手动测试、Test Explorer、测试覆盖率、IntelliTest和代码分析。特别是分析器,这是一款非常宝贵的工具,在社区版中可用。 15 | 16 | 如果正在设计Windows桌面应用程序,VS提供了可视编辑器和大量组件。对通用Windows平台(UWP)的支持非常广泛,这是Windows 10引入的Windows应用程序的UI标准。这种支持允许实现光滑、现代的设计,并且针对不同屏幕上的自适应控件进行了高度优化。 17 | 18 | 值得一提的是,尽管VS是一个仅限Windows的IDE,但仍然可以开发针对Linux和移动平台(Android和iOS)的项目。此外,还支持使用Windows本地库和Unreal Engine的游戏开发者。 19 | 20 | 准备好亲自体验它的工作方式了吗?以下是开始的方法。 21 | 22 | \mySubsubsection{3.4.2}{第一步} 23 | 24 | 这个IDE仅适用于Windows,并遵循标准的安装过程。首先从\url{https://visualstudio.microsoft.com/free-developeroffers/}下载安装程序。运行安装程序后,需要选择版本(社区版、专业版或企业版),并选择想要的安装的组件: 25 | 26 | \myGraphic{0.6}{content/chapter3/images/6.png}{图3.6:VS IDE安装程序窗口} 27 | 28 | 组件是允许VS支持特定语言、环境或程序格式的功能集,一些组件包括Python、Node.js或.NET。当然,我们感兴趣的是与C++相关的那些(图3.6);其针对不同的用例提供了广泛的支持: 29 | 30 | \begin{itemize} 31 | \item 32 | 使用C++的桌面开发 33 | 34 | \item 35 | 使用C++的通用Windows平台开发 36 | 37 | \item 38 | 使用C++的游戏开发 39 | 40 | \item 41 | 使用C++的移动开发 42 | 43 | \item 44 | 使用C++的Linux开发 45 | \end{itemize} 46 | 47 | 选择所需应用程序的选项并点击安装。不用担心安装所有选项——可以再次运行安装程序来修改选择。如果决定更精确地配置工作负载组件,请确保启用C++ CMake工具选项以获得对CMake的支持。 48 | 49 | 安装后,可以启动IDE并选择启动窗口上的“创建新项目”,将看到基于之前安装的工作负载的多个模板。要与CMake一起工作,请选择CMake项目模板。其他选项不一定使用CMake。创建项目后,可以通过点击窗口顶部绿色的播放按钮开启;代码将编译,将看到基本的程序执行,输出如下: 50 | 51 | \begin{shell} 52 | Hello CMake. 53 | \end{shell} 54 | 55 | 现在,准备好使用Visual Studio与CMake一起工作了码? 56 | 57 | \mySubsubsection{3.4.3}{高级功能:热重载调试} 58 | 59 | 运行Visual Studio可能会更耗资源并且启动时间更长,但它提供了许多无与伦比的功能。其中一个重要的改变者是热重载。以下是其使用方式:打开一个C++项目,使用调试器启动它,在代码文件中进行更改,点击热重载按钮(或Alt + F10),你的更改将立即在运行的应用程序中反映出来,同时保持状态。 60 | 61 | 为了确保热重载支持已启用,请在项目 > 属性 > C/C++ > 常规菜单中设置这两个选项: 62 | 63 | \begin{itemize} 64 | \item 65 | 调试信息格式必须设置为 \textbf{用于编辑并继续的程序数据库 /ZI} 66 | 67 | \item 68 | 启用增量链接必须设置为 \textbf{Yes /INCREMENTAL} 69 | \end{itemize} 70 | 71 | 热重载的背后机制可能看起来像魔法,但它是一个非常实用的功能。存在一些限制,例如:对全局/静态数据、对象布局或“时间旅行”更改(如更改已构建对象的构造函数)的更改。 72 | 73 | 更多关于热重载的信息可以在官方文档中找到:\url{https://learn.microsoft.com/en-us/visualstudio/debugger/hot-reload}。 74 | 75 | 这就是我们对这三个主要IDE的探索。起初的学习曲线可能看起来很陡峭,但如果投入学习这些IDE,当转向更高级的任务时,会很快看到回报的。 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /book/content/chapter3/5.tex: -------------------------------------------------------------------------------- 1 | 2 | 本章深入探讨了使用集成开发环境(IDE)来优化编程过程,特别是那些与CMake深度集成的IDE。它为初学者和有经验的专业人士提供了全面指南,详细介绍了使用IDE的好处,以及如何选择最适合个人或组织需求的IDE。 3 | 4 | 首先讨论了IDE在提升开发速度和代码质量方面的重要性,解释了什么是IDE,以及它如何通过整合代码编辑器、编译器和调试器等工具简化软件开发的各个步骤。接着,简要回顾了工具链,解释了如果系统中没有安装工具链的必要性,并列出了最常见的几个选择。 5 | 6 | 讨论了如何开始使用CLion及其独特的功能,并深入了解了其调试功能。VS Code是微软推出的一款免费、跨平台的IDE,以其庞大的扩展生态系统和对众多编程语言的支持而闻名。指导各位完成了初始设置和关键扩展的安装,并介绍了一个名为Dev Containers的高级功能。仅适用于Windows的VS IDE提供了一个精致、功能丰富的环境,适合各种用户需求。设置过程、关键特性和热加载调试功能也进行了介绍。 7 | 8 | 每个IDE部分都提供了为何选择特定IDE的见解,开始的步骤,以及使该IDE脱颖而出的高级功能。我们还强调了远程开发支持的概念,突出了其在行业中的日益重要性。 9 | 10 | 总结来说,本章为开发者提供了一个基础指南,帮助他们理解和选择IDE,清晰地概述了顶级选项、它们的独特优势,以及如何与CMake结合使用以提升编码效率和项目管理。 11 | 12 | 下一章中,我们将了解使用CMake进行项目设置的基础知识。 -------------------------------------------------------------------------------- /book/content/chapter3/6.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | \begin{itemize} 4 | \item 5 | Qt Creator IDE,另一个支持CMake的选项: 6 | 7 | \url{https://www.qt.io/product/development-tools} 8 | 9 | \item 10 | Eclipse IDE C/C++开发者,也支持CMake: 11 | 12 | \url{https://www.eclipse.org/downloads/packages/release/2023-12/r/eclipse-idecc-developers} 13 | 14 | \item 15 | macOS的Xcode也可以与CMake一起使用: 16 | 17 | \url{https://medium.com/practical-coding/migrating-to-cmake-in-c-and-gettingit-working-with-xcode-50b7bb80ae3d} 18 | 19 | \item 20 | CodeLite也是一个选择,有CMake插件: 21 | 22 | \url{https://docs.codelite.org/plugins/cmake/} 23 | \end{itemize} 24 | -------------------------------------------------------------------------------- /book/content/chapter3/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter3/images/1.png -------------------------------------------------------------------------------- /book/content/chapter3/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter3/images/2.png -------------------------------------------------------------------------------- /book/content/chapter3/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter3/images/3.png -------------------------------------------------------------------------------- /book/content/chapter3/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter3/images/4.png -------------------------------------------------------------------------------- /book/content/chapter3/images/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter3/images/5.png -------------------------------------------------------------------------------- /book/content/chapter3/images/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter3/images/6.png -------------------------------------------------------------------------------- /book/content/chapter4/0.tex: -------------------------------------------------------------------------------- 1 | 我们现在已经收集了足够的信息,可以开始讨论CMake的核心功能:构建项目。在CMake中,一个项目包含了所有源文件和必要的配置,以管理将解决方案变为现实的过程。配置过程从执行所有检查开始:验证目标平台是否受支持,确保所有必需的依赖项和工具的存在,并确认提供的编译器与所需特性兼容。 2 | 3 | 完成初步检查后,CMake继续生成一个定制的构建系统,该构建系统针对所选的构建工具。然后,执行构建编译源文件,并将它们与各自的依赖项链接在一起,以创建输出工件。 4 | 5 | 生成的工件可以通过不同的方式分发给消费者,可以直接与用户共享作为二进制包,允许用户使用包管理器将它们安装到自己的系统中。另一种方式是将其作为单一的可执行安装程序分发。此外,最终用户还可以通过访问开源仓库中的项目自行创建工件。这种情况下,用户可以使用CMake在自己的机器上编译项目,并随后进行安装。 6 | 7 | 充分利用CMake项目,可以显著提高开发体验和生成的代码的整体质量。通过利用CMake的力量,许多日常任务可以自动化,在构建后执行测试和运行代码覆盖检查器、格式化器、验证器、校验器和其他工具。这种自动化不仅节省了时间,还确保了一致性,并在整个开发过程中推广了代码质量。 8 | 9 | 要充分发挥CMake项目的潜力,首先需要做出一些关键决策:如何正确配置整个项目,以及如何分割项目和设置源树,以便所有文件都能整齐地组织在正确的目录中。从一开始就建立一个连贯的结构和组织,CMake项目就可以有效地管理和扩展。 10 | 11 | 接下来,我们将查看项目的构建环境。将了解我们正在使用的架构、可用的工具、支持的功能,以及正在使用的语言标准。为了确保一切同步,我们将编译一个测试C++文件,并查看选择的编译器是否符合我们为项目设定的标准要求。这一切都是为了确保项目、工具,以及选择标准之间的无缝配合。 12 | 13 | 本章中,将包含以下内容: 14 | 15 | \begin{itemize} 16 | \item 17 | 理解基本指令和命令 18 | 19 | \item 20 | 分割项目 21 | 22 | \item 23 | 项目结构 24 | 25 | \item 26 | 设置环境范围 27 | 28 | \item 29 | 配置工具链 30 | 31 | \item 32 | 禁用源内构建 33 | \end{itemize} -------------------------------------------------------------------------------- /book/content/chapter4/1.tex: -------------------------------------------------------------------------------- 1 | 2 | 可以在GitHub上的\url{https://github.com/PacktPublishing/Modern-CMake-for-Cpp-2E/tree/main/examples/ch04}找到本章中出现的代码文件。 3 | 4 | 为了构建本书提供的示例,请使用以下推荐命令: 5 | 6 | \begin{shell} 7 | cmake -B <build tree> -S <source tree> 8 | cmake --build <build tree> 9 | \end{shell} 10 | 11 | 请确保将占位符<build tree>和<source tree>替换为适当的路径。提醒一下:<build tree>是目标/输出目录的路径,而<source tree>是源码所在的目录。 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /book/content/chapter4/2.tex: -------------------------------------------------------------------------------- 1 | 第1章中,已经了解了一个简单的项目定义。这是一个带有CMakeLists.txt文件的目录,其中包含几个配置语言处理器的命令: 2 | 3 | \filename{chapter01/01-hello/CMakeLists.txt} 4 | 5 | \begin{cmake} 6 | cmake_minimum_required(VERSION 3.26) 7 | project(Hello) 8 | add_executable(Hello hello.cpp) 9 | \end{cmake} 10 | 11 | 在同一章节的“项目文件”部分,我们了解了一些基本命令。 12 | 13 | 现在,让我们在这里深入介绍它们。 14 | 15 | \mySubsubsection{4.2.1.}{指定最低CMake版本} 16 | 17 | 项目文件和脚本的顶部使用cmake\_minimum\_required()命令,这个命令不仅验证系统是否具有正确的CMake版本,还会隐式触发另一个命令cmake\_policy(VERSION),后者指定项目要使用的策略。这些策略定义了命令在CMake中的行为方式,它们随着CMake的发展,以及对支持的语言和CMake本身的改进而引入。 18 | 19 | 为了保持语言的清晰和简单,CMake团队在出现向后不兼容的更改时引入了策略。每个策略都启用了与该更改相关的新行为。这些策略确保项目可以适应CMake不断发展的特性和功能,同时保持与旧代码库的兼容性。 20 | 21 | 通过调用cmake\_minimum\_required(),我们告诉CMake需要应用在参数中提供的版本配置的默认策略。当CMake升级时,不必担心它会破坏项目,因为新版本的新策略不会启用。 22 | 23 | 策略可能会影响CMake的每一个方面,包括project()等其他重要命令。因此,在CMakeLists.txt文件开始时设置正在使用的版本非常重要;否则,将得到警告和错误。每个CMake版本都引入了许多策略。然而,除非在将旧项目升级到最新CMake版本时遇到挑战,否则没有必要深入细节。这种情况下,建议参考官方文档中关于策略的全面信息和指导:\url{https://cmake.org/cmake/help/latest/manual/cmake-policies.7.html}。 24 | 25 | \mySubsubsection{4.2.2.}{定义语言和元数据} 26 | 27 | 建议在cmake\_minimum\_required()之后立即放置project()命令,这样做将确保在配置项目时使用正确的策略。可以使用以下两种形式之一: 28 | 29 | \begin{shell} 30 | project(<PROJECT-NAME> [<language-name>...]) 31 | \end{shell} 32 | 33 | 或者: 34 | 35 | \begin{shell} 36 | project(<PROJECT-NAME> 37 | [VERSION <major>[.<minor>[.<patch>[.<tweak>]]]] 38 | [DESCRIPTION <project-description-string>] 39 | [HOMEPAGE_URL <url-string>] 40 | [LANGUAGES <language-name>...]) 41 | \end{shell} 42 | 43 | 需要指定<PROJECT-NAME>,但其他参数可选。调用此命令将隐式设置以下变量: 44 | 45 | \begin{shell} 46 | PROJECT_NAME 47 | CMAKE_PROJECT_NAME (only in the top-level CMakeLists.txt) 48 | PROJECT_IS_TOP_LEVEL, <PROJECT-NAME>_IS_TOP_LEVEL 49 | PROJECT_SOURCE_DIR, <PROJECT-NAME>_SOURCE_DIR 50 | PROJECT_BINARY_DIR, <PROJECT-NAME>_BINARY_DIR 51 | \end{shell} 52 | 53 | 支持哪些语言?相当多。而且可以在同一时间使用多种语言!以下是可以用来配置项目的语言关键字列表: 54 | 55 | \begin{itemize} 56 | \item 57 | ASM, ASM\_NASM, ASM\_MASM, ASMMARMASM, ASM-ATT: 汇编语言 58 | 59 | \item 60 | C: C 61 | 62 | \item 63 | CXX: C++ 64 | 65 | \item 66 | CUDA: Nvidia的统一计算设备架构 67 | 68 | \item 69 | OBJC: Objective-C 70 | 71 | \item 72 | OBJCXX: Objective-C++ 73 | 74 | \item 75 | Fortran: Fortran 76 | 77 | \item 78 | HIP: 用于Nvidia和AMD平台的异构(计算)接口便携性 79 | 80 | \item 81 | ISPC: 隐式SPMD程序编译器的语言 82 | 83 | \item 84 | CSharp: C\# 85 | 86 | \item 87 | Java: Java (需要额外步骤,请参阅官方文档) 88 | \end{itemize} 89 | 90 | CMake默认启用C和C++,因此可能只想为C++项目明确指定CXX。为什么?因为project()命令将检测和测试您选择的语言的可用编译器,所以声明所需的编译器,将在配置阶段跳过对未使用语言的检查,从而节省时间。 91 | 92 | 指定VERSION关键字将自动设置可以用来配置包的变量,或者在编译期间在头文件中暴露的变量: 93 | 94 | \begin{shell} 95 | PROJECT_VERSION, <PROJECT-NAME>_VERSION 96 | CMAKE_PROJECT_VERSION (only in the top-level CMakeLists.txt) 97 | PROJECT_VERSION_MAJOR, <PROJECT-NAME>_VERSION_MAJOR 98 | PROJECT_VERSION_MINOR, <PROJECT-NAME>_VERSION_MINOR 99 | PROJECT_VERSION_PATCH, <PROJECT-NAME>_VERSION_PATCH 100 | PROJECT_VERSION_TWEAK, <PROJECT-NAME>_VERSION_TWEAK 101 | \end{shell} 102 | 103 | 还可以设置DESCRIPTION和HOMEPAGE\_URL,这将为了类似目的设置以下变量: 104 | 105 | \begin{shell} 106 | PROJECT_DESCRIPTION, <PROJECT-NAME>_DESCRIPTION 107 | PROJECT_HOMEPAGE_URL, <PROJECT-NAME>_HOMEPAGE_URL 108 | \end{shell} 109 | 110 | cmake\_minimum\_required()和project()命令会创建一个基本的项目列表文件,并初始化一个空项目。虽然对于小型、单文件项目来说,结构可能不是一个大问题,但随着代码库的扩展,它变得至关重要。如何为此做准备呢? 111 | 112 | 113 | -------------------------------------------------------------------------------- /book/content/chapter4/3.tex: -------------------------------------------------------------------------------- 1 | 随着解决方案在代码行数和包含的文件数量上的增长,就必须面对一个挑战:要么开始划分项目,要么面临复杂性的风险。解决这个问题有两种方法:分割 CMake 代码,并将源文件重新定位到子目录中。在这两种情况下,目标都是遵循“关注点分离”的设计原则。简单来说,我们将代码分解成更小的部分,将紧密相关的功能组合在一起,同时保持其他代码段分离,以建立清晰的界限。 2 | 3 | 第一章中讨论列表文件时。简要提到了项目划分。我们谈到了 include() 命令,其允许 CMake 执行外部文件中的代码。 4 | 5 | 这种方法有助于分离关注点,但作用有限——专业代码提取到单独的文件中,甚至可以在不相关的项目之间共享,但如果作者不小心,其仍然会污染全局变量范围。 6 | 7 | 调用 include() 并不会在文件中,已经定义的范围之外引入任何范围或隔离。让我们通过一个例子来看看,为什么这是一个潜在的问题,这个例子是支持小型汽车租赁公司的软件。它将有多个源文件定义软件的不同方面:管理客户、汽车、停车位、长期合同、维护记录、员工记录等。如果所有这些文件都放在一个目录中,查找起来会是一场噩梦。因此,在项目的主目录中创建了一些目录,并将相关的文件移动到其中。我们的 CMakeLists.txt 文件看起来可能像这样: 8 | 9 | \filename{ch04/01-partition/CMakeLists.txt} 10 | 11 | \begin{cmake} 12 | cmake_minimum_required(VERSION 3.26.0) 13 | project(Rental CXX) 14 | add_executable(Rental 15 | main.cpp 16 | cars/car.cpp 17 | # more files in other directories 18 | ) 19 | \end{cmake} 20 | 21 | 这很棒,如你所见,我们仍然在顶层文件中包含了来自嵌套目录的源文件列表!为了增加关注点的分离,我们可以将源文件列表提取到另一个列表文件中,并将其存储在 sources 变量中: 22 | 23 | \filename{ch04/02-include/cars/cars.cmake} 24 | 25 | \begin{cmake} 26 | set(sources 27 | cars/car.cpp 28 | # more files in other directories 29 | ) 30 | \end{cmake} 31 | 32 | 现在我们可以使用 include() 命令引用这个文件,以访问 sources 变量: 33 | 34 | \filename{ch04/02-include/CMakeLists.txt} 35 | 36 | \begin{cmake} 37 | cmake_minimum_required(VERSION 3.26.0) 38 | project(Rental CXX) 39 | include(cars/cars.cmake) 40 | add_executable(Rental 41 | main.cpp 42 | ${sources} # for cars/ 43 | ) 44 | \end{cmake} 45 | 46 | CMake 会在与 add\_executable 相同的作用域中设置 sources,用所有文件填充该变量。这个解决方案有效,但有几个缺点: 47 | 48 | \begin{itemize} 49 | \item 50 | \textbf{嵌套目录中的变量将污染顶层作用域(反之亦然):} 一个简单的例子中这不是问题,但在更复杂的多级树结构中,使用了多个变量,这可以迅速成为一个难以调试的问题。如果有多个包含文件都定义了自己的 sources 变量呢? 51 | 52 | \item 53 | \textbf{所有目录将共享相同的配置:} 这个问题在项目经过多年的发展后会凸显。没有粒度,必须统一对待每个源文件,不能为代码的某些部分指定不同的编译标志,选择更新的语言版本,以及在代码的选定区域消除警告。一切都是全局的,所以需要同时对所有翻译单元进行更改。 54 | 55 | \item 56 | \textbf{存在共享的编译触发器:} 任何配置的更改都意味着所有文件都需要重新编译,即使对其中一些文件得更改无意义。 57 | 58 | \textbf{所有路径都是相对于顶级目录的:} 注意在 cars.cmake 中,我们必须提供到 cars/car.cpp 文件的完整路径。这导致了很多重复的文本,损害了可读性,并且违反了干净编码的“不要重复自己”(DRY)原则(不必要的重复会导致错误)。 59 | \end{itemize} 60 | 61 | 另一种方法是使用 add\_subdirectory() 命令,它引入了变量作用域,让我们来了解一下。 62 | 63 | \mySubsubsection{4.3.1.}{使用子目录管理作用域} 64 | 65 | 按照文件系统的自然结构来构建项目,是一种常见的做法,其中嵌套的目录代表应用程序的离散元素,业务逻辑、GUI、API 和报告,最后是包含测试、外部依赖、脚本和文档的独立目录。为了支持这个概念,CMake 提供了以下命令: 66 | 67 | \begin{shell} 68 | add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL]) 69 | \end{shell} 70 | 71 | 正如已经确立的,这将添加一个源目录到我们的构建中。可选地,我们可以提供一个路径,构建的文件将写入该路径(binary\_dir 或构建树)。EXCLUDE\_FROM\_ALL 关键字将禁用子目录中定义的目标的自动构建(将在下一章讨论目标)。这可能有用于分隔项目中不需要核心功能的部分(例如:示例或扩展)。 72 | 73 | add\_subdirectory() 将计算 source\_dir 路径(相对于当前目录)并解析其中的 CMakeLists.txt 文件。这个文件在目录作用域内解析,消除了前一种方法中提到的问题: 74 | 75 | \begin{itemize} 76 | \item 77 | 变量隔离到嵌套作用域。 78 | 79 | \item 80 | 嵌套工件可以独立配置。 81 | 82 | \item 83 | 修改嵌套的 CMakeLists.txt 文件不需要重新构建不相关的目标。 84 | 85 | \item 86 | 路径定位到目录,并且可以添加到父级包含路径。 87 | \end{itemize} 88 | 89 | 这是我们的 add\_subdirectory() 示例的目录结构: 90 | 91 | \begin{shell} 92 | |--CMakeLists.txt 93 | |-- cars 94 | | |-- CMakeLists.txt 95 | | |-- car.cpp 96 | | |-- car.h 97 | |-- main.cpp 98 | \end{shell} 99 | 100 | 这里,有两个 CMakeLists.txt 文件。顶层文件将使用嵌套目录 cars: 101 | 102 | \filename{ch04/03-add\_subdirectory/CMakeLists.txt} 103 | 104 | \begin{cmake} 105 | cmake_minimum_required(VERSION 3.26.0) 106 | project(Rental CXX) 107 | add_executable(Rental main.cpp) 108 | add_subdirectory(cars) 109 | target_link_libraries(Rental PRIVATE cars) 110 | \end{cmake} 111 | 112 | 最后一行用于将 cars 目录的工件链接到 Rental 可执行文件。这是一个特定于目标的命令,我们将在第 5 章中详细讨论。 113 | 114 | 来看看嵌套的列表文件长什么样: 115 | 116 | \filename{ch04/03-add\_subdirectory/cars/CMakeLists.txt} 117 | 118 | \begin{cmake} 119 | add_library(cars OBJECT 120 | car.cpp 121 | # more files in other directories 122 | ) 123 | target_include_directories(cars PUBLIC .) 124 | \end{cmake} 125 | 126 | 这个例子中,使用 add\_library() 来生成一个全局可见的目标 cars,并使用 target\_include\_directories() 将 cars 目录添加到其公共包含目录中。告诉 CMake cars.h 所在的位置,所以当使用 target\_link\_libraries() 时,main.cpp 文件可以在不提供相对路径的情况下使用该头文件: 127 | 128 | \begin{cpp} 129 | #include "car.h" 130 | \end{cpp} 131 | 132 | 我们可以在嵌套的列表文件中看到 add\_library() 命令,那在这个例子中开始使用库了吗?没有。我们使用了 OBJECT 关键字,表示只对生成对象文件(与上一个例子完全一样),只是将它们分组在一个逻辑目标(cars)下。你可能已经对目标有了一些概念,保持这个想法——我们将在下一章详细解释。 133 | 134 | \mySubsubsection{4.3.2.}{何时使用嵌套项目} 135 | 136 | 前一节中,简要提到了在 add\_subdirectory() 命令中使用的 EXCLUDE\_FROM\_ALL 参数,用于指示代码库中的外部元素。CMake 文档建议,如果有这样的部分存在于源树中,应该在其 CMakeLists.txt 文件中有自己的 project() 命令,以便生成自己的构建系统,且可以独立构建。 137 | 138 | 还有其他场景这种情况会有用吗?当然。例如,正在使用一个 CI/CD 构建的多个 C++ 项目时(可能是在构建框架或一组库时)。或者,正在将构建系统从传统的解决方案(如使用纯 makefile 的 GNU Make)移植过来。在这种情况下,可能希望有一个选项,慢慢地将事物分解成更独立的片段——将它们放入单独的构建过程中,或者只是在更小的范围内工作,就可以通过像 CLion 这样的 IDE 加载。可通过在嵌套目录的列表文件中,添加 project() 命令来实现这一点。只是不要忘记在它前面加上 cmake\_minimum\_required()。 139 | 140 | 既然支持项目嵌套,我们能否以某种方式连接并排构建的相关项目? 141 | 142 | \mySubsubsection{4.3.3.}{保持外部项目外部化} 143 | 144 | 虽然在 CMake 中从另一个项目引用内部,技术上可行,但这不是常规或推荐的做法。CMake 确实为这种情况提供了一些支持,包括使用 load\_cache() 命令从另一个项目的缓存中加载数据。然而,使用这种方法可能会导致循环依赖和项目耦合的问题。最好避免使用这个命令并做出决定:相关项目应该是嵌套的,通过库连接,还是合并成一个项目? 145 | 146 | 这些是我们可用的划分工具:包括引入列表文件、添加子目录和嵌套项目。但我们应该如何使用它们,以保持项目易于维护、导航和扩展呢?为此,我们需要一个明确定义的项目结构。 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /book/content/chapter4/4.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 随着项目的增长,要在项目列表文件和源码中找到所需内容变得越来越困难。因此,项目保持整洁就非常重要。 4 | 5 | 当需要交付一些重要且时间紧迫的更改,但这些更改不适合项目中的任一目录。现在,需要推送一个清理提交来重新组织文件结构,以便更改可以融入。或者更糟,决定随意放置,并添加一个TODO,以后再处理这个问题。 6 | 7 | 这些问题累积起来,随着技术债务的增加,维护代码的成本也随之上升。当实时系统出现严重故障需要快速修复,或者不熟悉代码库的人需要引入更改时,这种情况会变得非常棘手。 8 | 9 | 因此,需要一个好的项目结构。但这意味着什么呢?可以从软件开发的其他领域,如系统设计,借鉴一些规则。项目应该具有以下特点: 10 | 11 | \begin{itemize} 12 | \item 13 | 易于查找和扩展 14 | 15 | \item 16 | 边界清晰(项目特定文件应包含在项目目录中) 17 | 18 | \item 19 | 单个目标遵循层次树结构 20 | \end{itemize} 21 | 22 | 没有一个确定的解决方案,但在网上可用的各种项目结构模板中,我建议使用以下这个简单且可扩展的模板: 23 | 24 | \myGraphic{0.7}{content/chapter4/images/1.png}{图4.1:项目结构的一个示例} 25 | 26 | 此项目为以下组件定义了目录: 27 | 28 | \begin{itemize} 29 | \item 30 | cmake:共享宏和函数,find\_modules 和一次性脚本 31 | 32 | \item 33 | src: 二进制库文件,以及源文件和头文件 34 | 35 | \item 36 | test: 测试源码 37 | \end{itemize} 38 | 39 | 此结构中,CMakeLists.txt文件应存在于以下目录中:顶层项目目录、test和src及其所有子目录。主列表文件不应自行声明构建步骤,而应配置项目的一般方面,并通过add\_subdirectory()命令将构建责任委托给嵌套的列表文件。如有需要,这些列表文件可以进一步将工作委托给更深的层次。 40 | 41 | \begin{myNotic}{Note} 42 | 一些开发者建议将可执行文件与库分开,并创建两个顶级目录而不是一个:src和lib。CMake对这两种工件的处理方式相同,在这个层次上的分离并不重要。 43 | \end{myNotic} 44 | 45 | 对于较大的项目,src目录中有多个子目录会非常方便。但若只构建一个可执行文件或库,就可以省略,直接在src中存储源文件。无论如何,记得在那里添加一个CMakeLists.txt文件,并执行嵌套的列表文件。一个简单目标的文件树如下所示: 46 | 47 | \myGraphic{0.7}{content/chapter4/images/2.png}{图4.2:可执行文件的目录结构} 48 | 49 | 图4.1中,可以看到src目录根目录下有一个CMakeLists.txt文件——将配置关键的项目设置,并包含来自嵌套目录的所有列表文件。app1目录(如图4.2所示)包含另一个CMakeLists.txt文件,以及.cpp实现文件:class\_a.cpp和class\_b.cpp。还有一个main.cpp文件,其中包含可执行文件的入口。 50 | 51 | CMakeLists.txt文件应定义一个目标,使用这些源文件构建一个可执行文件——我们将在下一章学习如何做到这一点。 52 | 53 | 头文件放置在include目录中,可以用来为其他C++翻译单元声明符号。接下来,有一个仅对此可执行文件特定的库的lib3目录(项目中其他地方使用的库或对外部提供的库应位于src目录中)。这种结构提供了极大的灵活性,并可以轻松地扩展项目。随着添加更多类,可以方便地将它们分组到库中以提高编译速度。 54 | 55 | \myGraphic{0.7}{content/chapter4/images/3.png}{图4.3:库的目录结构} 56 | 57 | 库应遵循与可执行文件相同的结构:在include目录中添加了一个可选的lib1目录。当库打算超出项目范围时,会包含此目录,其包含其他项目在编译期间将使用的公共头文件。 58 | 59 | 我们已经讨论了文件如何在目录结构中布局。现在,是时候看看单个CMakeLists.txt文件如何组合成一个项目。 60 | 61 | \myGraphic{0.7}{content/chapter4/images/4.png}{图4.4:CMake如何将列表文件合并到单个项目中} 62 | 63 | 图中,每个框代表一个位于每个目录中的CMakeLists.txt列表文件,而其中的斜体标签代表每个文件执行的操作(从上到下)。再次从CMake的角度分析这个项目(有关详细信息,请查看ch04/05-structure目录中的示例): 64 | 65 | \begin{enumerate} 66 | \item 67 | 执行从项目的根开始——即源树顶层的CMakeLists.txt列表文件。该文件将设置所需的最低CMake版本和相应的策略,设置项目名称、支持的语言和全局变量,并包含cmake目录中的文件,以便内容全局可用。 68 | 69 | \item 70 | 下一步是调用add\_subdirectory(src bin)命令进入src目录的范围(希望将编译后的工件放在<binary\_tree>/bin,而不是/bin中)。 71 | 72 | \item 73 | CMake读取src/CMakeLists.txt文件,并发现其唯一目的是添加四个嵌套子目录:app1、app2、lib1和lib2。 74 | 75 | \item 76 | CMake进入app1的变量范围,并了解到另一个嵌套库lib3,它有自己的CMakeLists.txt文件;然后进入lib3的范围,这是对目录结构的深度优先遍历。 77 | 78 | \item 79 | lib3库添加了一个同名静态库目标,CMake返回到app1的父范围。 80 | 81 | \item 82 | app1子目录添加了一个依赖于lib3的可执行文件。CMake返回到src的父范围。 83 | 84 | \item 85 | CMake进入剩余的嵌套范围,并执行列表文件,直到所有add\_subdirectory()调用完成。 86 | 87 | \item 88 | CMake返回到顶层范围,并执行剩余的命令add\_subdirectory(test)。CMake每次都会进入新的范围,并执行相应列表文件中的命令。 89 | 90 | \item 91 | 收集并检查所有目标的正确性。CMake现在有了生成构建系统所需的所有信息。 92 | \end{enumerate} 93 | 94 | 前面的步骤按照在列表文件中编写命令的确切顺序发生。某些情况下,这个顺序很重要,有时可能没有那么关键。 95 | 96 | 那么,何时是创建包含项目所有元素的目录的正确时机呢?应该一开始就做——为未来创建所需的一切,并保持目录为空——还是等到实际上有了需要放入自己类别的文件再说?这是一个选择——可以遵循极限编程(XP)的规则YAGNI(你不会需要它),或者可以尝试使项目未来无忧,并为新来的开发者打下良好的基础。 97 | 98 | 尝试在这两种方法之间找到良好的平衡——如果怀疑项目可能有一天需要extern目录,那就添加(版本控制系统可能需要一个空的.keep文件来将目录签入仓库)。 99 | 100 | 另一种有效的做法是,通过创建README文件来指导他人放置他们的外部依赖,该文件概述了推荐的结构。这对于将来将要处理项目的经验不足的开发者有益。可能已经观察到了:开发者不愿意创建目录,尤其是在项目的根目录下。如果提供了一个好的项目结构,其他人就会倾向于遵循它。 101 | 102 | 有些项目几乎可以在任何环境中构建,而有些项目的要求非常特殊。顶层列表文件是确定适当行动方案的最佳位置。 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /book/content/chapter4/6.tex: -------------------------------------------------------------------------------- 1 | 对于CMake项目,工具链包括用于构建和运行应用程序的所有工具——工作环境、生成器、CMake可执行文件,以及编译器。 2 | 3 | 当构建因为一些神秘的编译和语法错误而停止时,一个经验较少的用户会感到多么困惑。他们必须深入源代码,试图理解发生了什么。 4 | 5 | 经过一个小时的调试,发现正确的解决方案是更新编译器。 6 | 7 | 我们能否为用户提供更好的体验,并在开始构建之前检查编译器是否支持所有必需的功能?当然可以!我们有方法指定这些要求。如果工具链不支持所有必需的功能,CMake将提前停止并显示清晰的错误消息,要求用户介入。 8 | 9 | \mySubsubsection{4.6.1.}{设置C++标准} 10 | 11 | 我们可能考虑的第一个步骤是,指定编译器应支持的所需C++标准,以构建我们的项目。对于新项目,建议设置C++14作为最低标准,但最好是C++17或C++20。从CMake 3.20开始,如果编译器支持,可以设置所需的标准为C++23。此外,从CMake 3.25开始,还有一个选项可以将标准设置为C++26,尽管这目前只是一个占位符。 12 | 13 | \begin{myNotic}{Note} 14 | 自C++11正式发布以来已经超过10年,它不再是现代C++标准。除非目标环境非常老旧,否则不建议以这个版本开始项目 15 | \end{myNotic} 16 | 17 | 另一个坚持旧标准的原因可能是,如果正在构建难以升级的遗留目标。然而,C++委员会非常努力地保持C++向后兼容,通常将标准升级到更高版本都不会有问题。 18 | 19 | CMake支持按目标逐一设置标准(这对于代码库的某些部分确实非常古老的部分很有用),但最好在整个项目中统一标准。这可以通过将CMAKE\_CXX\_STANDARD变量设置为以下值来实现:98、11、14、17、20、23或26,例如: 20 | 21 | \begin{cmake} 22 | set(CMAKE_CXX_STANDARD 23) 23 | \end{cmake} 24 | 25 | 这将作为随后定义的所有目标的默认值(最好将其设置在根列表文件的上方)。如果需要,可以对每个目标进行重写: 26 | 27 | \begin{shell} 28 | set_property(TARGET <target> PROPERTY CXX_STANDARD <version>) 29 | \end{shell} 30 | 31 | 或者: 32 | 33 | \begin{shell} 34 | set_target_properties(<targets> PROPERTIES CXX_STANDARD <version>) 35 | \end{shell} 36 | 37 | 第二种方式允许在需要时指定多个目标。 38 | 39 | \mySubsubsection{4.6.2.}{坚持标准支持} 40 | 41 | 前一部分提到的CXX\_STANDARD属性不会阻止CMake继续构建,即使编译器不支持所需的版本——视为一个偏好。CMake不知道代码是否用了,以前编译器中不可用的那些新特性。 42 | 43 | 如果我们确信这将不会成功,可以设置另一个变量(以与前一个相同的方式在每个目标上可重写),明确要求我们针对的标准: 44 | 45 | \begin{cmake} 46 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 47 | \end{cmake} 48 | 49 | 如果系统中的编译器不支持所需的标准,用户将看到以下消息,并且构建将停止: 50 | 51 | \begin{shell} 52 | Target "Standard" requires the language dialect "CXX23" (with compiler extensions), but CMake does not know the compile flags to use to enable it. 53 | \end{shell} 54 | 55 | 要求C++23可能有点过,即使对于现代环境也是如此。但是C++20对于最新的系统来说应该可以,因为自从2021/2022年以来,GCC/Clang/MSVC普遍都支持。 56 | 57 | \mySubsubsection{4.6.3.}{特定供应商的扩展} 58 | 59 | 根据组织中实施的政策,可能会对允许或禁用特定供应商的扩展感兴趣。这些是什么呢?可以说C++标准对于某些编译器生产商的需求来说进展缓慢,所以他们决定为语言添加他们自己的功能——可以称之为扩展。例如,C++技术报告1(TR1)是一个库扩展,引入了正则表达式、智能指针、哈希表和随机数生成器,这些在成为常见功能之前就已经存在。为了支持GNU项目发布的此类插件,CMake将标准编译器标志(-std=c++14)替换为-std=gnu++14。 60 | 61 | 一方面,其允许一些方便的功能。另一方面,代码将失去可移植性,因为在切换到不同的编译器(或者用户这样做时)可能无法构建!这也是一个按目标设置的属性,其中有一个默认变量,CMAKE\_CXX\_EXTENSIONS。CMake在这方面更加宽松,默认允许扩展,除非明确告诉它不要这样做: 62 | 63 | \begin{cmake} 64 | set(CMAKE_CXX_EXTENSIONS OFF) 65 | \end{cmake} 66 | 67 | 如果可能的话,我建议这样做,因为这将坚持使用与供应商无关的代码。这样的代码不会给用户带来不必要的限制。类似于之前的选项,可以使用set\_property()在每个目标上更改这个值。 68 | 69 | \mySubsubsection{4.6.4.}{过程间优化} 70 | 71 | 编译器在单个翻译单元级别优化代码,所以.cpp文件将进行预处理、编译,然后优化。在这些操作中生成的中间文件,然后传递给链接器,以创建单个二进制文件。然而,现代编译器在链接时,具有执行跨过程优化的能力,也称为链接时优化。这允许所有编译单元作为一个统一的模块进行优化,原则上会得到更好的结果(有时以构建速度更慢和内存消耗更多为代价)。 72 | 73 | 如果编译器支持过程间优化,那就使用它。我们将遵循相同的方法,负责此设置的变量称为CMAKE\_INTERPROCEDURAL\_OPTIMIZATION。但在设置它之前,需要确保编译器是否支持: 74 | 75 | \begin{cmake} 76 | include(CheckIPOSupported) 77 | check_ipo_supported(RESULT ipo_supported) 78 | set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ${ipo_supported}) 79 | \end{cmake} 80 | 81 | 需要包含一个内置模块才能访问check\_ipo\_supported()命令。如果优化不支持,这段代码将优雅地失败,并回退到默认行为。 82 | 83 | \mySubsubsection{4.6.5.}{检查支持的编译器特性} 84 | 85 | 正如之前讨论的,如果构建失败,最好是在早期失败,这样就可以向用户提供清晰的反馈消息,并缩短等待时间。有时特别关心哪些C++特性支持(哪些不支持),CMake将在配置阶段询问编译器,并将可用特性列表存储在CMAKE\_CXX\_COMPILE\_FEATURES变量中。我们可以编写一个非常具体的检查,并询问是否支持某个特性: 86 | 87 | \filename{ch04/07-features/CMakeLists.txt} 88 | 89 | \begin{cmake} 90 | list(FIND CMAKE_CXX_COMPILE_FEATURES cxx_variable_templates result) 91 | if(result EQUAL -1) 92 | message(FATAL_ERROR "Variable templates are required for compilation.") 93 | endif() 94 | \end{cmake} 95 | 96 | 为每个我们使用的特性编写一个检查是一项艰巨的任务。甚至CMake的作者也建议只检查某些高级元特性是否存在:cxx\_std\_98、cxx\_std\_11、cxx\_std\_14、cxx\_std\_17、cxx\_std\_20、cxx\_std\_23和cxx\_std\_26。每个元特性都表示编译器支持特定的C++标准,可以像之前示例中那样使用。 97 | 98 | CMake知道的所有特性的完整列表可以在文档中找到:\url{https://cmake.org/cmake/help/latest/prop_gbl/CMAKE_CXX_KNOWN_FEATURES.html}。 99 | 100 | \mySubsubsection{4.6.6.}{编译测试文件} 101 | 102 | 当我在使用GCC 4.7.x编译应用程序时,发生了一个特别有趣的情况。我已经在编译器的参考中手动确认了所有C++11特性都得到了支持。然而,解决方案仍然无法正确工作。代码默默地忽略了标准头文件的调用。结果发现,这个特定的编译器存在一个bug,正则表达式库没有实现。 103 | 104 | 没有检查可以避免这类罕见错误(而且你不应该需要检查它们!),但可能会想要使用最新标准的一些尖端实验特性,而又不知道哪些编译器支持。可以通过创建一个使用特殊特性的测试文件,来测试项目是否能够工作,这个文件可以快速编译和执行。 105 | 106 | CMake提供了两个配置时间命令,try\_compile()和try\_run(),以验证目标平台上所需的一切是否得到支持。 107 | 108 | try\_run()命令会给予更多的自由,因为它可以确保代码不仅编译成功,而且执行也正确(可能想要测试正则表达式是否工作)。当然,这不会在交叉编译场景中工作(因为主机无法运行为不同目标构建的可执行文件),这个检查的目的是向用户提供快速反馈(能正常编译),所以它不是用来运行单元测试或任何复杂的东西——文件尽可能简单: 109 | 110 | \filename{ch04/08-test\_run/main.cpp} 111 | 112 | \begin{cpp} 113 | #include <iostream> 114 | int main() 115 | { 116 | std::cout << "Quick check if things work." << std::endl; 117 | } 118 | \end{cpp} 119 | 120 | 调用try\_run()并不复杂。首先设置所需的标准,然后调用try\_run(),并将收集的信息输出给用户: 121 | 122 | \filename{ch04/08-test\_run/CMakeLists.txt} 123 | 124 | \begin{cmake} 125 | set(CMAKE_CXX_STANDARD 20) 126 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 127 | set(CMAKE_CXX_EXTENSIONS OFF) 128 | try_run(run_result compile_result 129 | ${CMAKE_BINARY_DIR}/test_output 130 | ${CMAKE_SOURCE_DIR}/main.cpp 131 | RUN_OUTPUT_VARIABLE output) 132 | message("run_result: ${run_result}") 133 | message("compile_result: ${compile_result}") 134 | message("output:\n" ${output}) 135 | \end{cmake} 136 | 137 | 这个命令看起来挺吓人,但实际上只需要几个参数就可以编译,并运行一个非常基本的测试文件。我还使用了可选的RUN\_OUTPUT\_VARIABLE关键字来收集stdout的输出。 138 | 139 | 下一步是扩展基本测试文件,使用实际项目中使用的C++特性——比如通过添加一个变长模板,来查看目标机器上的编译器是否能够处理它。 140 | 141 | 最后,可以在条件块中检查收集的输出是否符合预期,并在出现问题时打印message(SEND\_ERROR)。SEND\_ERROR关键字将允许CMake继续配置阶段,但将阻止生成构建系统。这在显示所有遇到的错误后才终止构建之前非常有用。我们现在知道如何确保编译可以完整完成。 142 | 143 | 接下来,让我们转向下一个主题,禁用源内构建。 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /book/content/chapter4/7.tex: -------------------------------------------------------------------------------- 1 | 2 | 第1章中,我们讨论了源内构建,以及如何建议总是指定构建路径在源码之外。这不仅允许更清晰的构建树和更简单的.gitignore文件,而且还能降低意外覆盖或删除源文件的风险。 3 | 4 | 若要提前停止构建,可以使用以下检查: 5 | 6 | \filename{ch04/09-in-source/CMakeLists.txt} 7 | 8 | \begin{cmake} 9 | cmake_minimum_required(VERSION 3.26.0) 10 | project(NoInSource CXX) 11 | if(PROJECT_SOURCE_DIR STREQUAL PROJECT_BINARY_DIR) 12 | message(FATAL_ERROR "In-source builds are not allowed") 13 | endif() 14 | message("Build successful!") 15 | \end{cmake} 16 | 17 | 如果想要了解更多关于STR前缀和变量引用的信息,请复习第2章。 18 | 19 | 需要注意的是,无论前面的代码中做了什么,CMake似乎仍然会创建一个CMakeFiles/目录和一个CMakeCache.txt文件。 20 | 21 | \begin{myNotic}{Note} 22 | 可能会在线上找到建议使用未记录的变量,来确保用户无论如何都不能在源目录中写入。不建议依赖未记录的变量来限制在源目录中写入,它们可能不会在所有版本中都有效,并且可能会在没有警告的情况下被移除或修改。 23 | \end{myNotic} 24 | 25 | 如果担心用户将这些文件留在源目录中,将它们添加到.gitignore(或等效的文件中),并更改消息,请求手动清理。 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /book/content/chapter4/8.tex: -------------------------------------------------------------------------------- 1 | 本章中,包含了为构建健壮且面向未来的项目打下坚实基础的重要概念。讨论了设置最低CMake版本和配置项目,如名称、语言和元数据字段。这些可使项目能够有效地扩展。 2 | 3 | 探讨了项目分区,比较了基本的include()与add\_subdirectory的使用,后者提供了诸如范围变量管理、简化路径和提高模块化等好处。创建嵌套项目并分别构建它们的能力,在逐渐将代码分解为更独立的单元时。证明了其价值。在理解了分区机制后,又深入研究了如何创建透明、健壮且可扩展的项目结构。检查了CMake遍历列表文件和配置步骤的正确顺序,并研究了如何限定目标机器和宿主机器的环境,它们之间的区别是什么,以及可以通过不同的查询获得关于平台和系统的哪些信息。我们还了解了配置工具链,包括指定所需的C++版本,处理特定供应商的编译器扩展,以及启用重要的优化。最后,了解了如何测试编译器所需的功能,并执行示例文件以测试编译支持。 4 | 5 | 目前为止,了解的技术对于项目至关重要,但它们不足以使项目真正有用。为了增加项目的实用性,需要理解目标的概念。我们之前简要地触及了这个话题,而现在,已经对相关基础知识有了扎实的理解,就可以对其进行全面地探讨。 6 | 7 | 下一章将介绍的目标,将在进一步增强我们项目的功能性和有效性方面发挥关键作用。 -------------------------------------------------------------------------------- /book/content/chapter4/9.tex: -------------------------------------------------------------------------------- 1 | 2 | \begin{itemize} 3 | \item 4 | 关注点分离: 5 | 6 | \url{https://nalexn.github.io/separation-of-concerns/} 7 | 8 | \item 9 | 完整的CMake变量参考文档: 10 | 11 | \url{https://cmake.org/cmake/help/latest/manual/cmake-variables.7.html} 12 | 13 | \item 14 | try\_compile和try\_run文档: 15 | 16 | \url{https://cmake.org/cmake/help/latest/command/try_compile.html}, \url{https://cmake.org/cmake/help/latest/command/try_run.html} 17 | 18 | \item 19 | CheckIPOSupported参考文档: 20 | 21 | \url{https://cmake.org/cmake/help/latest/module/CheckIPOSupported.html} 22 | \end{itemize} 23 | -------------------------------------------------------------------------------- /book/content/chapter4/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter4/images/1.png -------------------------------------------------------------------------------- /book/content/chapter4/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter4/images/2.png -------------------------------------------------------------------------------- /book/content/chapter4/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter4/images/3.png -------------------------------------------------------------------------------- /book/content/chapter4/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter4/images/4.png -------------------------------------------------------------------------------- /book/content/chapter5/0.tex: -------------------------------------------------------------------------------- 1 | CMake中,整个应用程序可以从单个源代码文件(如经典的helloworld.cpp)构建。但同样也可以创建一个项目,其中可执行文件是由许多源文件构建的:几十个甚至几千个。许多初学者都是这样做的:用几个文件构建二进制文件,让项目在没有严格计划的情况下自然增长。根据需要不断添加文件,所有东西都已经直接链接到一个没有结构的单一二进制文件中。 2 | 3 | 作为软件开发者,我们故意划界限,并将组件指定为将一个或多个翻译单元(.cpp文件)分组的部分,是为了提高代码的可读性,管理耦合和关联性,加快构建过程,并最终发现和提取可重用组件成为自治单元。 4 | 5 | 每个大型项目都会引入某种形式的划分,这里就是CMake目标发挥作用的地方。CMake目标代表了一个专注于特定目标的逻辑单元。目标可以依赖于其他目标,构建遵循声明方法。CMake负责确定构建目标的正确顺序,尽可能优化并行构建,并相应地执行必要步骤。作为一个通用原则,当一个目标构建时,会生成一个工件,该工件可以作为其他目标使用,或作为构建过程的最终输出。 6 | 7 | 注意“工件”这个词,我故意避免使用特定术语,因为CMake在生成可执行文件或库之外提供了灵活性。实际上,可以利用生成的构建系统来产生各种类型的输出:额外的源文件、头文件、目标文件、存档、配置文件等。唯一的要求是一个命令行工具(如编译器)、可选的输入文件,以及指定的输出路径。 8 | 9 | 目标是极其强大的概念,极大地简化了构建项目的过程。理解其如何工作,并掌握以优雅和有组织的方式配置技巧,也至关重要。这些知识确保了顺畅和高效的开发体验。 10 | 11 | 本章中,包含以下内容: 12 | 13 | \begin{itemize} 14 | \item 15 | 理解目标 16 | 17 | \item 18 | 设置目标的属性 19 | 20 | \item 21 | 编写自定义命令 22 | \end{itemize} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /book/content/chapter5/1.tex: -------------------------------------------------------------------------------- 1 | 可以在GitHub上的\url{https://github.com/PacktPublishing/Modern-CMake-for-Cpp-2E/tree/main/examples/ch05}找到本章中出现的代码文件。 2 | 3 | 为了构建本书提供的示例,请使用推荐的命令: 4 | 5 | \begin{shell} 6 | cmake -B <build tree> -S <source tree> 7 | cmake --build <build tree> 8 | \end{shell} 9 | 10 | 请确保将<build tree>和<source tree>占位符替换为适当的路径。提醒一下:<build tree>是目标/输出目录的路径,而<source tree>是源码所在的位置。 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /book/content/chapter5/3.tex: -------------------------------------------------------------------------------- 1 | 2 | 使用自定义目标有一个缺点——将它们添加到ALL目标或者依赖它们来构建其他目标,每次都会构建。有时候,这正是想要的,但是对于以下操作,需要根据情况判断是否要执行构建: 3 | 4 | \begin{itemize} 5 | \item 6 | 生成另一个目标所依赖的源代码文件 7 | 8 | \item 9 | 将另一种语言翻译成C++ 10 | 11 | \item 12 | 在另一个目标构建之前或之后,立即执行自定义操作 13 | \end{itemize} 14 | 15 | 自定义命令有两个签名。第一个是add\_custom\_target()的扩展版本: 16 | 17 | \begin{shell} 18 | add_custom_command(OUTPUT output1 [output2 ...] 19 | COMMAND command1 [ARGS] [args1...] 20 | [COMMAND command2 [ARGS] [args2...] ...] 21 | [MAIN_DEPENDENCY depend] 22 | [DEPENDS [depends...]] 23 | [BYPRODUCTS [files...]] 24 | [IMPLICIT_DEPENDS <lang1> depend1 25 | [<lang2> depend2] ...] 26 | [WORKING_DIRECTORY dir] 27 | [COMMENT comment] 28 | [DEPFILE depfile] 29 | [JOB_POOL job_pool] 30 | [VERBATIM] [APPEND] [USES_TERMINAL] 31 | [COMMAND_EXPAND_LISTS]) 32 | \end{shell} 33 | 34 | 与自定义目标一样,自定义命令也不创建逻辑目标,且必须添加到依赖关系图中。有两种方法可以实现向依赖关系图的添加——将其输出工件作为可执行文件(或库)的源,或者显式地将其添加到调用自定义目标或命令指令时的DEPENDS列表中。 35 | 36 | \mySubsubsection{5.3.1.}{将自定义命令用作生成器} 37 | 38 | 诚然,并非每个项目都需要从其他文件生成C++代码。这样的情况可能是编译Google的Protocol Buffer(Protobuf)的.proto文件。如果不熟悉这个库,Protobuf是一个用于结构化数据的平台无关的二进制序列化器。 39 | 40 | 可以用来将对象编码和解码为二进制流:文件或网络连接。为了保持Protobuf跨平台并且在同时保持快速,Google的工程师们发明了自己的Protobuf语言,在.proto文件中定义模型,如下所示: 41 | 42 | \begin{shell} 43 | message Person { 44 | required string name = 1; 45 | required int32 id = 2; 46 | optional string email = 3; 47 | } 48 | \end{shell} 49 | 50 | 这样的文件可以用来在多种语言中编码数据——C++、Ruby、Go、Python、Java等等。Google提供了编译器——protoc,读取.proto文件并为所选语言输出有效的结构和序列化源代码(稍后需要编译或解释)。聪明的工程师不会将这些生成的源文件检入仓库,而是使用原始的Protobuf格式并在构建链中添加一个步骤来生成源文件。 51 | 52 | 我们尚不知道如何检测目标主机上是否(以及在哪里)可用Protobuf编译器(我们将在第9章中介绍)。现在,假设编译器的protoc命令位于系统已知的某个位置,已经准备好了person.proto文件,并且知道Protobuf编译器将输出person.pb.h和person.pb.cc文件。以下是如何定义一个自定义命令来编译它们的例子: 53 | 54 | \begin{cmake} 55 | add_custom_command(OUTPUT person.pb.h person.pb.cc 56 | COMMAND protoc ARGS person.proto 57 | DEPENDS person.proto 58 | ) 59 | \end{cmake} 60 | 61 | 然后,为了允许可执行文件进行序列化,可以简单地将输出文件添加到源文件中: 62 | 63 | \begin{cmake} 64 | add_executable(serializer serializer.cpp person.pb.cc) 65 | \end{cmake} 66 | 67 | 假设正确处理了头文件的包含和Protobuf库的链接,当对.proto文件进行更改时,一切都会自动编译和更新。 68 | 69 | 一个简化的(实用性要小得多)示例是通过从另一个位置复制来创建必要的头文件: 70 | 71 | \filename{ch05/03-command/CMakeLists.txt} 72 | 73 | \begin{cmake} 74 | add_executable(main main.cpp constants.h) 75 | target_include_directories(main PRIVATE ${CMAKE_BINARY_DIR}) 76 | add_custom_command(OUTPUT constants.h COMMAND cp 77 | ARGS "${CMAKE_SOURCE_DIR}/template.xyz" constants.h) 78 | \end{cmake} 79 | 80 | 这时,“编译器”是cp命令。它通过从源树中复制到构建树根目录,创建一个constants.h文件,从而满足main目标的依赖。 81 | 82 | \mySubsubsection{5.3.2.}{将自定义命令用作目标钩子} 83 | 84 | add\_custom\_command()命令的第二个版本引入了一种机制,用于在构建目标之前或之后执行命令: 85 | 86 | \begin{cmake} 87 | add_custom_command(TARGET <target> 88 | PRE_BUILD | PRE_LINK | POST_BUILD 89 | COMMAND command1 [ARGS] [args1...] 90 | [COMMAND command2 [ARGS] [args2...] ...] 91 | [BYPRODUCTS [files...]] 92 | [WORKING_DIRECTORY dir] 93 | [COMMENT comment] 94 | [VERBATIM] [USES_TERMINAL] 95 | [COMMAND_EXPAND_LISTS]) 96 | \end{cmake} 97 | 98 | 通过第一个参数指定想要用新行为“增强”的目标,并在满足以下条件的情况下执行: 99 | 100 | \begin{itemize} 101 | \item 102 | PRE\_BUILD 将在此目标的所有其他规则之前运行(仅限Visual Studio生成器;对于其他生成器,行为类似于PRE\_LINK)。 103 | 104 | \item 105 | PRE\_LINK 将命令绑定在所有源代码编译完成后,但在链接(或归档)目标之前运行。不适用于自定义目标。 106 | 107 | \item 108 | POST\_BUILD 将在此目标的所有其他规则执行完毕后运行。 109 | \end{itemize} 110 | 111 | 使用这个版本的add\_custom\_command(),可以复现之前BankApp示例中的校验和生成: 112 | 113 | \filename{ch05/04-command/CMakeLists.txt} 114 | 115 | \begin{cmake} 116 | cmake_minimum_required(VERSION 3.26) 117 | project(Command CXX) 118 | add_executable(main main.cpp) 119 | add_custom_command(TARGET main POST_BUILD 120 | COMMAND cksum 121 | ARGS "$<TARGET_FILE:main>" > "main.ck") 122 | \end{cmake} 123 | 124 | 主可执行文件的构建完成后,CMake将使用提供的参数执行cksum。但是第一个参数中发生了什么?它不是一个变量,如果是变量,它会被大括号(\$\{\})包裹,而不是尖括号(\$<>)。它是一个生成器表达式,计算结果为目标二进制文件的完整路径。这种机制在许多目标属性的上下文中非常有用,我们将在下一章中详细解释。 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /book/content/chapter5/4.tex: -------------------------------------------------------------------------------- 1 | 理解目标,是编写清晰、现代的CMake项目的关键。本章中,不仅讨论了什么是目标,以及如何定义三种不同类型的目标:可执行文件、库和自定义目标。还解释了目标如何通过依赖关系图相互依赖,并了解了如何使用Graphviz模块可视化。有了这种基本的理解,就能够了解目标的关键特性——属性。我们不仅介绍了一些在目标上设置常规属性的命令,还解决了传播属性,也称为“目标传递的使用要求”谜团。 2 | 3 | 这是一个难以攻克的问题,因为需要理解的不仅是如何控制传播哪些属性,还有这种传播如何影响后续的目标。此外,还发现了如何确保从多源属性的兼容性。 4 | 5 | 然后,简要讨论了伪目标:导入的目标、别名目标和接口库。所有这些在以后的项目中都会派上用场,特别是当了解如何将它们与传播属性连接起来。接着,讨论了生成的构建目标,以及对配置阶段的影响。之后,花了一些时间研究一种与目标相似,但又不完全是目标的机制:自定义命令。提到了如何生成其他目标(编译、翻译等)使用的文件,以及其钩子功能:在构建目标时执行的步骤。 6 | 7 | 有了如此坚实的基础,就可以进行下一个主题了——将C++源代码编译成可执行文件和库。 -------------------------------------------------------------------------------- /book/content/chapter5/5.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | \begin{itemize} 4 | \item 5 | Graphviz模块的文档: 6 | 7 | \url{https://gitlab.kitware.com/cmake/community/-/wikis/doc/cmake/Graphviz}, \url{https://cmake.org/cmake/help/latest/module/CMakeGraphVizOptions.html} 8 | 9 | \item 10 | Graphviz软件: 11 | 12 | \url{https://graphviz.org} 13 | 14 | \item 15 | CMake目标属性: 16 | 17 | \url{https://cmake.org/cmake/help/latest/manual/cmake-properties.7.html#properties-on-targets} 18 | 19 | \item 20 | 传递目标的使用要求: 21 | 22 | \url{https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#target-usage-requirements} 23 | \end{itemize} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /book/content/chapter5/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter5/images/1.png -------------------------------------------------------------------------------- /book/content/chapter5/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter5/images/2.png -------------------------------------------------------------------------------- /book/content/chapter5/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter5/images/3.png -------------------------------------------------------------------------------- /book/content/chapter6/0.tex: -------------------------------------------------------------------------------- 1 | 许多CMake用户在探索中并未遇到生成器表达式,这个概念相当高级。然而,对于准备进入普遍可用阶段,或首次向更广泛受众发布的项目来说,生成器表达式在导出、安装和打包方面起着重要的作用。如果只是想快速学习CMake的基础知识并专注于C++方面,可以暂时跳过本章,以后再回来阅读。另一方面,我们现在讨论生成器表达式,因为接下来的章节在解释CMake更深入的内容时,会引用这些知识。 2 | 3 | 我们将从介绍生成器表达式的主题开始:它们是什么,有什么用途,以及如何形成和扩展的。接下来将简要介绍嵌套机制,并对条件扩展进行更详细的描述,可以使用布尔逻辑、比较操作和查询。当然,会将深入探讨表达式的内容。 4 | 5 | 首先,研究字符串、列表和路径的转换,专注于主要内容之前,了解基础知识很有必要。最终,生成器表达式在实际应用中用来获取在构建后期阶段可用的信息,并在适当的上下文中呈现。确定这个上下文有很大的价值,将了解如何根据用户选择的构建配置、当前平台和当前工具链,来参数化构建过程。也就是说,要确定正在使用的编译器、其版本,以及具有的功能。不仅如此,还将找出如何查询构建目标及其相关信息的属性。 6 | 7 | 为了确保能够充分理解生成器表达式,我在本章的最后部分包含了一些有趣的使用示例。还有一个关于如何查看生成器表达式输出的快速解释,这有点棘手。不过别担心,生成器表达式并不像看起来那么复杂。 8 | 9 | 本章中,将包含以下内容: 10 | 11 | \begin{itemize} 12 | \item 13 | 生成器表达式是什么? 14 | 15 | \item 16 | 通用表达式语法的基本规则 17 | 18 | \item 19 | 条件扩展 20 | 21 | \item 22 | 查询和转换 23 | 24 | \item 25 | 尝试示例 26 | \end{itemize} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /book/content/chapter6/1.tex: -------------------------------------------------------------------------------- 1 | 2 | 可以在GitHub上的\url{https://github.com/PacktPublishing/Modern-CMake-for-Cpp-2E/tree/main/examples/ch06}找到本章中出现的代码文件。 3 | 4 | 为了构建本书提供的示例,请使用推荐的命令: 5 | 6 | \begin{shell} 7 | cmake -B <build tree> -S <source tree> 8 | cmake --build <build tree> 9 | \end{shell} 10 | 11 | 请确保将<build tree>和<source tree>占位符替换为适当的路径。提醒一下:<build tree>是目标/输出目录的路径,而<source tree>是源码所在的位置路径。 12 | -------------------------------------------------------------------------------- /book/content/chapter6/2.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | CMake在三个阶段构建解决方案:配置、生成和运行构建工具。通常,配置阶段所需数据都可以找到。然而,有时会遇到类似于“先有鸡还是先有蛋”的悖论。以第5章中的“将自定义命令作为目标钩子”的例子来说,一个目标需要知道另一个目标的二进制工件的路径。不幸的是,这个信息只有在所有列表文件解析且配置阶段完成后才能获得。 4 | 5 | 那如何解决这类问题呢?一个解决方案是为这个信息创建一个占位符,并将其计算推迟到下一个阶段——生成阶段。 6 | 7 | 这正是生成器表达式(也称为“genexes”)所做的。其主要围绕目标属性(如LINK\_LIBRARIES、INCLUDE\_DIRECTORIES、COMPILE\_DEFINITIONS)及传播属性创建,遵循类似于条件语句和变量计算的规则。 8 | 9 | \begin{myNotic}{Note} 10 | 生成器表达式将在生成阶段进行计算(配置完成且构建系统创建后),所以将它们的输出捕获到变量,并输出到控制台并不是直接的操作。 11 | \end{myNotic} 12 | 13 | 生成器表达式的数量众多,在某种程度上构成了自己的、特定领域的语言——这种语言支持条件表达式、逻辑操作、比较、转换、查询和排序。利用生成器表达式可以操作和查询字符串、列表、版本号、shell路径、配置和构建目标。本章中,将简要概述这些概念,由于在大多数情况下不是必需的,所以将专注于基础知识。主要关注点将是生成器表达式的应用,即从目标的生成配置和构建环境状态中收集信息。为了完整参考,最好在线阅读官方的CMake手册(请参见“扩展阅读”部分获取URL)。 14 | 15 | 通过例子解释都会更清楚,所以让我们直接进入正题,描述生成器表达式的语法。 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /book/content/chapter6/3.tex: -------------------------------------------------------------------------------- 1 | 2 | 要使用生成器表达式,需要将其添加到支持生成器表达式计算的CMake命令中。大多数特定于目标的命令都支持,还有许多其他命令(查看特定命令的官方文档以了解更多信息)。 3 | 4 | 一个经常与生成器异常一起使用的命令是target\_compile\_definitions()。要使用生成器表达式,需要将其作为命令参数提供,如下所示: 5 | 6 | \begin{cmake} 7 | target_compile_definitions(foo PUBLIC BAR=$<TARGET_FILE:baz>) 8 | \end{cmake} 9 | 10 | 这个命令向编译器的参数中添加了一个-D定义标志(先忽略PUBLIC),将BAR预处理器定义设置为foo目标产生的二进制工件的路径(生成器表达式以当前形式存储在变量中)。这种扩展推迟到了生成阶段,那时许多事情都已经配置并已知。 11 | 12 | 生成器表达式是什么样的呢? 13 | 14 | \myGraphic{0.4}{content/chapter6/images/1.png}{图6.1:生成器表达式的语法} 15 | 16 | 如图6.1所示,结构看起来相当简单: 17 | 18 | \begin{itemize} 19 | \item 20 | 以美元符号和左括号(\$<)开始。 21 | 22 | \item 23 | 添加EXPRESSION名称。 24 | 25 | \item 26 | 如果表达式需要参数,添加冒号(:)并提供arg1, arg2 … argN值,用逗号(,)分隔。 27 | 28 | \item 29 | 以大于号(>)结束表达式。 30 | \end{itemize} 31 | 32 | 有些表达式不需要参数,例如\$<PLATFORM\_ID>。 33 | 34 | 除非明确指出,否则表达式通常是在使用表达式的目标上下文中进行计算的。这种关联是从使用表达式的命令中推断出的。前面的例子中,了解了target\_compile\_definitions()是如何提供foo作为其操作的目标。因此,在该命令中使用的特定于目标的生成器表达式将隐式地使用foo。注意,示例中使用的生成器表达式\$<TARGET\_FILE>需要目标属性作为其操作的上下文。还有一些生成器表达式不接受目标作为参数(如\$<COMPILE\_LANGUAGE>),将隐式地使用封闭命令的目标。这些将在后面详细讨论。 35 | 36 | 当使用生成器表达式的更高级功能时,可能会很快变得非常混乱和复杂,事先了解其细节信息非常重要。 37 | 38 | \mySubsubsection{6.3.1.}{嵌套} 39 | 40 | 让我们从生成式表达式的嵌套开始介绍。即支持将生成器表达式作为参数,传递给另一个生成器表达式: 41 | 42 | \begin{shell} 43 | $<UPPER_CASE:$<PLATFORM_ID>> 44 | \end{shell} 45 | 46 | 这个例子并不复杂,但很容易想象当增加嵌套级别,并在使用多个参数的命令中工作时会发生什么。 47 | 48 | 为了进一步复杂化,可以将常规变量在其中展开: 49 | 50 | \begin{shell} 51 | $<UPPER_CASE:${my_variable}> 52 | \end{shell} 53 | 54 | 变量my\_variable将首先在配置阶段展开,生成器表达式将在生成阶段展开。这个特性有一些罕见的使用场景,但我强烈建议避免使用这种方式:生成器表达式提供了几乎所有必要的功能。将这些常规变量混合到表达式中增加了一层难以调试的间接性。此外,在配置阶段收集的信息通常会过时,因为用户会在构建或安装阶段通过命令行参数覆盖生成器表达式中使用的值。 55 | 56 | 了解了语法之后,让我们继续讨论生成器表达式中的基本机制。 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /book/content/chapter6/4.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 可以执行逻辑运算,确定是否应该展开生成式表达式的值。这是一个很棒的功能,但因为历史原因,其语法可能不一致且难以阅读。它有两种形式,第一种形式支持正面和负面路径: 4 | 5 | \begin{shell} 6 | $<IF:condition,true_string,false_string> 7 | \end{shell} 8 | 9 | IF 表达式依赖于生成式表达式的嵌套:可以将任何一个参数替换为另一个表达式,并产生相当复杂的计算(甚至可以在一个 IF 条件中嵌套另一个)。这种形式需要正好三个参数,所以不能省略任何东西。可以通过如下形式表示在不满足condition条件时不返回任何值: 10 | 11 | \begin{shell} 12 | $<IF:condition,true_string,> 13 | \end{shell} 14 | 15 | 有一个简写版本可以省略 IF 关键字和逗号: 16 | 17 | \begin{shell} 18 | $<condition:true_string> 19 | \end{shell} 20 | 21 | 其打破了将表达式名称作为第一个标记提供的惯例。我猜这里的意图是为了缩短表达式,并避免输入那些宝贵的几个字符,但结果可能真的很难理性化。以下是从 CMake 文档中取出的一例: 22 | 23 | \begin{shell} 24 | $<$<AND:$<COMPILE_LANGUAGE:CXX>,$<CXX_COMPILER_ID:AppleClang,Clang>>:COMPILING_CXX_WITH_CLANG> 25 | \end{shell} 26 | 27 | 这个表达式只有在用 Clang 编译器编译的 C++ 代码中才返回 COMPILING\_CXX\_WITH\_CLANG(其他情况下返回空字符串)。我希望这个语法能够与常规 IF 命令的条件保持一致,但遗憾的是情况并非如此。现在,如果在某个地方看到了第二种形式,知道它是怎么工作的就好;但为了可读性,应该避免在自己的项目中使用。 28 | 29 | \mySubsubsection{6.4.1.}{计算布尔值} 30 | 31 | 生成器表达式计算为两种类型之一——布尔值或字符串。布尔值用 1(真)和 0(假)表示。没有专用的数值类型;除了布尔值之外的类型都只是字符串。 32 | 33 | 需要记住的是,作为条件表达式中的条件传递的嵌套表达式,明确要求计算为布尔值。 34 | 35 | 布尔类型可以隐式转换为字符串,但是字符串转换为布尔类型需要使用明确的 BOOL 运算符(稍后解释)。 36 | 37 | 有三类表达式可计算为布尔值:逻辑运算符、比较表达式和查询。 38 | 39 | \mySamllsection{逻辑运算符} 40 | 41 | 有四个逻辑运算符: 42 | 43 | \begin{itemize} 44 | \item 45 | \$<NOT:arg>: 否定布尔参数。 46 | 47 | \item 48 | \$<AND:arg1,arg2,arg3...>: 如果所有参数都为真,则返回真。 49 | 50 | \item 51 | \$<OR:arg1,arg2,arg3...>: 如果任一参数为真,则返回真。 52 | 53 | \item 54 | \$<BOOL:string\_arg>: 这将字符串参数从字符串转换为布尔类型。 55 | \end{itemize} 56 | 57 | 使用\$<BOOL>的字符串转换在以下条件都不满足时,将计算为布尔真(1): 58 | 59 | \begin{itemize} 60 | \item 61 | 字符串为空。 62 | 63 | \item 64 | 字符串是 0、FALSE、OFF、N、NO、IGNORE 或 NOTFOUND 的不区分大小写的等价物。 65 | 66 | \item 67 | 字符串以 -NOTFOUND 后缀结尾(区分大小写)。 68 | \end{itemize} 69 | 70 | \mySamllsection{比较} 71 | 72 | 如果满足条件,比较将计算为 1,否则为 0。以下是一些可能有用的常见操作: 73 | 74 | \begin{itemize} 75 | \item 76 | \$<STREQUAL:arg1,arg2>: 这以区分大小写的方式比较字符串。 77 | 78 | \item 79 | \$<EQUAL:arg1,arg2>: 这将字符串转换为数字并比较相等性。 80 | 81 | \item 82 | \$<IN\_LIST:arg,list>: 这检查 arg 元素是否在 list 列表中(区分大小写)。 83 | 84 | \item 85 | \$<VERSION\_EQUAL:v1,v2>,\$<VERSION\_LESS:v1,v2>, \$<VERSION\_GREATER:v1,v2>,\$<VERSION\_LESS\_EQUAL:v1,v2> 和 \$<VERSION\_GREATER\_EQUAL:v1,v2> 以逐组件的方式比较版本。 86 | 87 | \item 88 | \$<PATH\_EQUAL:path1,path2>: 这比较两个路径的词法表示,不进行标准化(自 CMake 3.24 起)。 89 | \end{itemize} 90 | 91 | \mySamllsection{查询} 92 | 93 | 查询直接从变量返回布尔值,或者作为操作的结果。 94 | 95 | 最简单的查询之一是: 96 | 97 | \begin{shell} 98 | $<TARGET_EXISTS:arg> 99 | \end{shell} 100 | 101 | 如果目标在配置阶段定义,将返回真。 102 | 103 | 现在,知道如何应用条件展开,使用逻辑运算符、比较和基本查询来计算为布尔值。但生成器表达式还有更多功能,特别是在查询的上下文中:可以在 IF 条件展开中使用,或者作为参数独立地传递给命令。 104 | 105 | 是时候在适当的上下文中介绍它们了。 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /book/content/chapter6/6.tex: -------------------------------------------------------------------------------- 1 | 2 | 有了好的实际例子,理论就更容易掌握。显然,我们想要编写一些 CMake 代码并尝试一下。然而,由于生成器表达式直到配置完成之后才进行计算,不能使用配置时的命令,如 message() 来验证。为了调试生成器表达式需要使用一些特殊的技巧,可以使用以下方法: 3 | 4 | \begin{itemize} 5 | \item 6 | 将其写入文件(file() 命令的特定版本支持生成器表达式): file(GENERATE OUTPUT filename CONTENT "\$<...>") 7 | 8 | \item 9 | 添加一个自定义目标,并显式构建: add\_custom\_target(gendbg COMMAND \$\{CMAKE\_COMMAND\} -E echo "\$<...>") 10 | \end{itemize} 11 | 12 | 我推荐第一个选项进行简单的练习。记住,我们无法在这些命令中使用所有的表达式,因为有些是特定于目标的。 13 | 14 | 了解了这一点,再来看一下生成器表达式的某些用途。 15 | 16 | \mySubsubsection{6.6.1.}{构建配置} 17 | 18 | 在第1章中,讨论了构建类型,指定正在构建的配置——Debug、Release 等。可能有些情况下,希望根据正在进行的构建类型进行不同的操作。一个简单且直接的方法是使用\$<CONFIG>生成器表达式: 19 | 20 | \begin{shell} 21 | target_compile_options(tgt $<$<CONFIG:DEBUG>:-ginline-points>) 22 | \end{shell} 23 | 24 | 前面的例子检查配置是否等于 DEBUG;如果是这样,嵌套表达式计算为 1。然后外部的简写 if 表达式变为真,-ginline-points 调试标志就添加到选项中。 25 | 26 | 了解这种形式很重要,这样就能够理解其他项目中的此类表达式,但我建议为了更好的可读性,可以使用更详细的\$<IF:...>. 27 | 28 | \mySubsubsection{6.6.2.}{系统特定的单行命令} 29 | 30 | 生成器表达式还可以用来将冗长的 if 命令压缩成整洁的单行命令。 31 | 32 | 假设我们有以下代码: 33 | 34 | \begin{cmake} 35 | if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") 36 | target_compile_definitions(myProject PRIVATE LINUX=1) 37 | endif() 38 | \end{cmake} 39 | 40 | 它告诉编译器,如果这是目标系统,则向参数中添加 -DLINUX=1。虽然这并不算太长,但可以用一个相当简单的表达式替换: 41 | 42 | \begin{cmake} 43 | target_compile_definitions(myProject PRIVATE 44 | $<$<CMAKE_SYSTEM_NAME:LINUX>:LINUX=1>) 45 | \end{cmake} 46 | 47 | 这样的代码运行良好,但可以在生成器表达式中打包的内容有限,直到变得难以阅读。此外,许多 CMake 用户会推迟学习生成器表达式,并难以跟踪发生的事情。幸运的是,在完成本章后,我们就不会遇到这样的问题。 48 | 49 | \mySubsubsection{6.6.3.}{带有编译器特定标志的接口库} 50 | 51 | 如第5章所讨论,接口库可以用来提供与编译器匹配的标志: 52 | 53 | \begin{cmake} 54 | add_library(enable_rtti INTERFACE) 55 | target_compile_options(enable_rtti INTERFACE 56 | $<$<OR:$<COMPILER_ID:GNU>,$<COMPILER_ID:Clang>>:-rtti> 57 | ) 58 | \end{cmake} 59 | 60 | 即使如此简单的例子中,当嵌套太多生成器表达式时,理解表达式也会变得难以理解。但有时这是实现期望效果的唯一方式。以下是例子的解释: 61 | 62 | \begin{itemize} 63 | \item 64 | 检查 COMPILER\_ID 是否为 GNU;如果是,将 OR 计算为 1。 65 | 66 | \item 67 | 如果不是,检查 COMPILER\_ID 是否为 Clang,并将 OR 计算为 1。否则,将 OR 评估为 0。 68 | 69 | \item 70 | 如果 OR 计算为 1,将 -rtti 添加到 enable\_rtti 编译选项中。否则,不做任何事情。 71 | \end{itemize} 72 | 73 | 接下来,可以将库和可执行文件与 enable\_rtti 接口库链接起来。如果编译器支持,CMake 将添加 -rtti 标志。RTTI 代表运行时类型信息,在 C++ 中与 typeid 等关键字一起使用,以在运行时确定对象的类;除非代码使用这个功能,否则不需要启用该标志。 74 | 75 | \mySubsubsection{6.6.4.}{嵌套生成器表达式} 76 | 77 | 有时,会尝试在生成器表达式中嵌套元素,但会发生什么并不明显。我们可以通过生成一个测试,将其输出到调试文件来调试表达式。 78 | 79 | 看看会发生什么: 80 | 81 | \filename{ch06/01-nesting/CMakeLists.txt} 82 | 83 | \begin{cmake} 84 | set(myvar "small text") 85 | set(myvar2 "small text >") 86 | 87 | file(GENERATE OUTPUT nesting CONTENT " 88 | 1 $<PLATFORM_ID> 89 | 2 $<UPPER_CASE:$<PLATFORM_ID>> 90 | 3 $<UPPER_CASE:hello world> 91 | 4 $<UPPER_CASE:${myvar}> 92 | 5 $<UPPER_CASE:${myvar2}> 93 | ") 94 | \end{cmake} 95 | 96 | 在构建此项目后,可以使用 Unix 的 cat 命令读取生成的 nesting 文件: 97 | 98 | \begin{shell} 99 | # cat nesting 100 | 101 | 1 Linux 102 | 2 LINUX 103 | 3 HELLO WORLD 104 | 4 SMALL TEXT 105 | 5 SMALL text> 106 | \end{shell} 107 | 108 | 这就是每行的工作内容: 109 | 110 | \begin{enumerate} 111 | \item 112 | PLATFORM\_ID 的输出值是 LINUX。 113 | 114 | \item 115 | 嵌套值的输出将被正确地转换为大写的 LINUX。 116 | 117 | \item 118 | 可以转换普通字符串。 119 | 120 | \item 121 | 可以转换配置阶段的变量内容。 122 | 123 | \item 124 | 变量将首先插值,然后闭合的尖括号(>)将解释为生成器表达式的一部分,只有字符串的部分大写。 125 | \end{enumerate} 126 | 127 | 注意,变量的内容可能会影响生成器表达式的扩展行为。如果需要在变量中使用尖括号,请使用\$<ANGLE-R>。 128 | 129 | \mySubsubsection{6.6.5.}{布尔表达式与 BOOL 运算符的计算差异} 130 | 131 | 当涉及到将布尔类型计算为字符串时,生成器表达式可能会有些令人困惑。了解它们与常规条件表达式的区别至关重要,尤其是从 IF 关键字开始: 132 | 133 | \filename{ch06/02-boolean/CMakeLists.txt} 134 | 135 | \begin{cmake} 136 | cmake_minimum_required(VERSION 3.26) 137 | project(Boolean CXX) 138 | 139 | file(GENERATE OUTPUT boolean CONTENT " 140 | 1 $<0:TRUE> 141 | 2 $<0:TRUE,FALSE> (won't work) 142 | 3 $<1:TRUE,FALSE> 143 | 4 $<IF:0,TRUE,FALSE> 144 | 5 $<IF:0,TRUE,> 145 | ") 146 | \end{cmake} 147 | 148 | 使用 Linux 的 cat 命令读取生成的文件: 149 | 150 | \begin{enumerate} 151 | \item 152 | 这是一个布尔展开,其中 BOOL 是 0;因此,TRUE 字符串不会写入。 153 | 154 | \item 155 | 这是一个典型的错误——作者原本打算根据 BOOL 值打印 TRUE 或 FALSE,但也是一个布尔假的展开,两个参数当作一个参数处理,因此不会输出。 156 | 157 | \item 158 | 这是相同错误的一个反转值——它是一个布尔真的展开,两者都在同一行写入。 159 | 160 | \item 161 | 这是一个以 IF 开头的正确条件表达式——输出 FALSE,因为第一个参数是 0。 162 | 163 | \item 164 | 这是条件表达式的正确用法,但当不需要为布尔假提供值时,应该使用第一行中的方式。 165 | \end{enumerate} 166 | 167 | 生成器表达式因其复杂的语法而声名狼藉,本例中提到的差异甚至可能会让经验丰富的构建者感到困惑。如果有所疑虑,将这样的表达式复制到另一个文件中,并通过添加缩进和空格来分析,以更好地理解。 168 | 169 | 了解了生成器表达式的工作示例,从而为我们使用它们做好了准备。接下来的章节将讨论许多与生成器表达式相关的主题。 170 | 171 | 172 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /book/content/chapter6/7.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 本章全部内容都是关于剖析生成器表达式(或称“genexes”)的细节。我们从生成器表达式的形成和扩展的基础知识开始,并查了解了它的嵌套机制。深入探讨了条件扩展的强大功能,其利用了布尔逻辑、比较操作和查询。当根据用户选择的构建配置、平台和当前工具链等因素调整构建过程时,生成器表达式的这一方面尤为突出。 4 | 5 | 我们还了解了字符串、列表和路径的基本但重要的转换。一个重点亮点是使用查询在后期构建阶段收集的信息,并在上下文符合要求时呈现。现在也知道如何检查编译器的ID、版本和功能。探索了使用生成器表达式查询构建目标属性,并提取了相关信息。并以实用的示例和可能的输出查看指南作为结尾。有了这些,就可以在项目中使用生成器表达式了。 6 | 7 | 下一章中,将学习如何使用CMake编译程序。具体来说,将讨论如何配置和优化这个过程。 -------------------------------------------------------------------------------- /book/content/chapter6/8.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | \begin{itemize} 4 | \item 5 | 官方文档中的生成器表达式: 6 | 7 | \url{https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html} 8 | 9 | \item 10 | 支持的编译器ID: 11 | 12 | \url{https://cmake.org/cmake/help/latest/variable/CMAKE_LANG_COMPILER_ID.html} 13 | 14 | \item 15 | 在CMake中混合使用多种语言: 16 | 17 | \url{https://stackoverflow.com/questions/8096887/mixing-c-and-c-with-cmake} 18 | \end{itemize} -------------------------------------------------------------------------------- /book/content/chapter6/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter6/images/1.png -------------------------------------------------------------------------------- /book/content/chapter7/0.tex: -------------------------------------------------------------------------------- 1 | 简单的编译场景通常由工具链的默认配置处理,或者由集成开发环境(IDE)直接提供。专业环境中,商业需求往往需要更高级的功能。可能是对更高性能、更小的二进制文件、更好的可移植性、自动化测试或广泛的调试功能的需求——不胜枚举。要以连贯且面向未来的方式管理所有这些需求,很快就会变得复杂而混乱(特别是在需要支持多个平台时)。 2 | 3 | 编译过程在C++书籍中通常没有解释得足够清楚(深入主题,如虚基类,似乎更有趣)。本章中,将通过探讨编译的不同方面来纠正这一点:将了解编译如何工作,其内部阶段是什么,以及如何影响二进制输出。 4 | 5 | 之后,将关注先决条件——讨论可以使用哪些命令来微调编译过程,如何要求编译器具有特定功能,以及如何正确指导编译器处理哪些输入文件。 6 | 7 | 然后,关注编译的第一阶段——预处理器。提供包含头文件的路径,并且将学习如何通过预处理器定义将CMake和构建环境中的变量插入。我们将涵盖最有趣的使用案例,并学习如何暴露CMake变量,以便它们可以在C++中访问。 8 | 9 | 紧接着,将讨论优化器,以及不同的标志如何影响性能。还将讨论优化的成本,特别是它如何影响产生的二进制的可调试性,以及如果不希望这样该怎么办。 10 | 11 | 最后,将解释如何通过使用预编译头文件和统一构建来管理编译过程,以减少编译时间。并将了解如何调试构建过程,并找出可能犯的错误。 12 | 13 | 本章中,将包含以下内容: 14 | 15 | \begin{itemize} 16 | \item 17 | 编译基础 18 | 19 | \item 20 | 配置预处理器 21 | 22 | \item 23 | 配置优化器 24 | 25 | \item 26 | 管理编译过程 27 | \end{itemize} 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /book/content/chapter7/1.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 可以在GitHub上的以下链接找到本章中出现的代码文件:\url{https://github.com/PacktPublishing/Modern-CMake-for-Cpp-2E/tree/main/examples/ch07}。 4 | 5 | 为了构建本书提供的示例,请使用推荐的命令: 6 | 7 | \begin{shell} 8 | cmake -B <build tree> -S <source tree> 9 | cmake --build <build tree> 10 | \end{shell} 11 | 12 | 请确保将<build tree>和<source tree>占位符替换为适当的路径。提醒一下:<build tree>是目标/输出目录的路径,而<source tree>是源码所在的位置。 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /book/content/chapter7/6.tex: -------------------------------------------------------------------------------- 1 | 我们已经完成了又一章的学习!毫无疑问,编译是一个复杂的过程。考虑到所有的边缘情况和特定要求,如果没有一个强大的工具,管理起来可能会很困难。幸运的是,CMake在这里为我们提供了支持。 2 | 3 | 那么,到目前为止我们学到了什么?从讨论编译是什么,以及它如何适应在操作系统中构建和运行应用程序开始。然后,检查了编译的各个阶段,以及管理它们的内部工具。这对于解决未来可能遇到的问题价值连城。 4 | 5 | 接下来,探讨了如何使用CMake来验证宿主上可用的编译器,是否满足代码构建的必要要求。正如已经建立的,对于解决方案的用户来说,最好看到一个友好的消息提示他们升级,而不是一个过时的编译器输出的神秘错误。后者无法处理语言的新特性,这无疑是显著更好的体验。 6 | 7 | 我们简要讨论了如何向已定义的目标添加源文件,然后继续讨论预处理的配置。这是一个相当重要的主题,因为这一阶段将所有代码片段汇集在一起,并确定哪些部分将忽略。我们讨论了提供文件路径和添加自定义定义的方法,既可以单独添加,也可以批量添加(以及一些使用案例)。 然后,讨论了优化器,探索了所有一般优化级别,以及它们隐式添加的标志。还详细讨论了其中的一些——finline、floop-unroll 和 ftree-vectorize。 8 | 9 | 最后,研究如何管理编译的可行性。在这里解决了两个主要方面——减少编译时间(这进而有助于保持程序员的专注)和查找错误。后者对于识别什么是错误的,以及为什么错误极为重要。正确设置工具并理解为什么会发生这些事情,对于确保代码质量(以及保持心理健康)做出了巨大贡献。 10 | 11 | 下一章中,我们将了解链接,以及构建库,并在我们的项目中使用构建的库。 -------------------------------------------------------------------------------- /book/content/chapter7/7.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | \begin{itemize} 4 | \item 5 | CMake支持的编译特性和编译器: 6 | 7 | \url{https://cmake.org/cmake/help/latest/manual/cmake-compile-features.7.html#supported-compilers} 8 | 9 | \item 10 | 管理目标的源文件: 11 | 12 | \url{https://stackoverflow.com/questions/32411963/why-is-cmake-file-glob-evil}, \url{https://cmake.org/cmake/help/latest/command/target_sources.html} 13 | 14 | \item 15 | \#include关键字: 16 | 17 | \url{https://en.cppreference.com/w/cpp/preprocessor/include} 18 | 19 | \item 20 | 提供包含文件的路径: 21 | 22 | \url{https://cmake.org/cmake/help/latest/command/target_include_directories.html} 23 | 24 | \item 25 | 配置头文件: 26 | 27 | \url{https://cmake.org/cmake/help/latest/command/configure_file.html} 28 | 29 | \item 30 | 头文件的预编译: 31 | 32 | \url{https://cmake.org/cmake/help/latest/command/target_precompile_headers.html} 33 | 34 | \item 35 | 统一构建: 36 | 37 | \url{https://cmake.org/cmake/help/latest/prop_tgt/UNITY_BUILD.html} 38 | 39 | \item 40 | 预编译头文件的统一构建: 41 | 42 | \url{https://www.qt.io/blog/2019/08/01/precompiled-headers-and-unity-jumbo-builds-in-upcoming-cmake} 43 | 44 | \item 45 | 查找错误——编译器标志: 46 | 47 | \url{https://interrupt.memfault.com/blog/best-and-worst-gcc-clang-compiler-flags} 48 | 49 | \item 50 | 为什么使用库文件而不是对象文件: 51 | 52 | \url{https://stackoverflow.com/questions/23615282/object-files-vs-libraryfiles-and-why} 53 | 54 | \item 55 | 关注点分离: 56 | 57 | \url{https://nalexn.github.io/separation-of-concerns/} 58 | \end{itemize} -------------------------------------------------------------------------------- /book/content/chapter8/0.tex: -------------------------------------------------------------------------------- 1 | 当我们成功地将源代码编译成二进制文件,貌似作为构建工程师的角色就完成了。然而,事实并非如此。虽然二进制文件确实包含了CPU执行所需的所有代码,但这些代码以复杂的方式分布在多个文件中。我们不希望CPU在不同的文件中搜索单个代码片段。相反,是要将这些单独的单元合并成一个文件。为了实现这一点,需要使用一个称为“链接”的过程。 2 | 3 | CMake的链接命令并不多,其中target\_link\_libraries()其中之一。那为什么还要为单个命令写一整个章节呢?在计算机科学中,几乎没有什么东西是简单的,链接也不例外:为了得到正确的结果,需要对整体进行了解——需要知道链接器如何工作,并且要掌握基础知识。本章将讨论目标文件的内部结构,如何工作的重定位和引用解析机制,以及其用途。还会讨论最终可执行文件与组件的不同之处,以及系统在将程序加载到内存时,如何构建进程映像。 4 | 5 | 然后,介绍各种类型的库:静态库、共享库和共享模块。它们都称为“库”,但又非常不同。创建链接良好的可执行文件,依赖于正确的配置和处理特定细节,例如:位置无关代码(PIC)。 6 | 7 | 了解链接的另一个难题——唯一定义规则(ODR)。准确地定义符号的数量至关重要。管理重复的符号很具有挑战性,尤其是对于共享库。此外,将探讨为什么链接器有时无法定位外部符号,即使可执行文件已正确链接到相关库。 8 | 9 | 最后,将了解如何高效地使用链接器,为解决方案在特定框架内进行测试做好准备。 10 | 11 | 本章中,包含以下内容: 12 | 13 | \begin{itemize} 14 | \item 15 | 链接的基本知识 16 | 17 | \item 18 | 构建不同类型的库 19 | 20 | \item 21 | 解决ODR问题 22 | 23 | \item 24 | 链接和未解析符号的顺序 25 | 26 | \item 27 | 为测试分离main() 28 | \end{itemize} 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /book/content/chapter8/1.tex: -------------------------------------------------------------------------------- 1 | 可以在GitHub上找到本章中存在的代码文件,网址为 \url{https://github.com/PacktPublishing/Modern-CMake-for-Cpp-2E/tree/main/examples/ch08}。 2 | 3 | 要构建本书中提供的示例,请使用推荐的命令: 4 | 5 | \begin{shell} 6 | cmake -B <build tree> -S <source tree> 7 | cmake --build <build tree> 8 | \end{shell} 9 | 10 | 请确保用适当的路径替换<build tree>和<source tree>占位符。作为提醒:<build tree>是目标/输出目录的路径,<source tree>是源码所在的路径。 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /book/content/chapter8/2.tex: -------------------------------------------------------------------------------- 1 | 在第7章中,我们讨论了 C++ 程序的生命周期,它由五个主要阶段组成——编写、编译、链接、加载和执行。正确编译所有源代码后,需要将它们组合成一个可执行文件。编译产生的对象文件不能直接由处理器执行,为什么会这样呢? 2 | 3 | 为了回答这个问题,我们要知道对象文件是广泛使用的可执行和链接格式(ELF)的一种变体,这在类 Unix 系统和其他许多系统中都很常见。像 Windows 或 macOS 这样的系统有自己的格式,但我们将专注于 ELF 来解释这个原理。图 8.1 显示了编译器如何构建这些文件: 4 | 5 | \myGraphic{0.9}{content/chapter8/images/1.png}{图 8.1:对象文件的结构} 6 | 7 | 编译器将为每个翻译单元(每个 .cpp 文件)准备一个对象文件,这些文件将用于构建程序的内存映像。对象文件包含以下内容: 8 | 9 | \begin{itemize} 10 | \item 11 | ELF 头部,标识目标操作系统(OS)、文件类型、目标指令集架构,以及 ELF 文件中两个头部表的位置和大小的详细信息:程序头部表(在对象文件中不存在)和节头部表。 12 | 13 | \item 14 | 按类型分组信息的二进制节。 15 | 16 | \item 17 | 节头部表,包含关于名称、类型、标志、内存中的目标地址、文件中的偏移量,以及其他信息。用于了解这个文件中有哪些节,以及它们的位置,就像目录一样。 18 | \end{itemize} 19 | 20 | 当编译器处理源代码时,将收集的信息分类到不同的节中。这些节构成了 ELF 文件的核心,位于 ELF 头部和节头部之间。以下是一些例子: 21 | 22 | \begin{itemize} 23 | \item 24 | .text 节包含所有指定给处理器执行的机器代码指令。 25 | 26 | \item 27 | .data 节保存初始化的全局和静态变量的值。 28 | 29 | \item 30 | .bss 节为未初始化的全局和静态变量保留空间,这些变量在程序开始时初始化为零。 31 | 32 | \item 33 | .rodata 节保存常量的值,使其成为一个只读数据段。 34 | 35 | \item 36 | .strtab 节是一个字符串表,包含常量字符串,例如:来自基本 hello.cpp 示例的“Hello World”。 37 | 38 | \item 39 | .shstrtab 节是一个字符串表,保存所有其他节的名字。 40 | \end{itemize} 41 | 42 | 这些反映了最终放入 RAM 运行的可执行文件。然而,不能简单地将对象文件连接在一起,然后将结果文件加载到内存中。不加考虑的合并会导致一系列复杂问题,例如:会浪费空间和时间,消耗过多的 RAM 页面。将指令和数据传输到 CPU 缓存也会变得笨拙,整个系统将不得不处理增加的复杂性,浪费宝贵的周期。执行过程会在无数的 .text、.data 和其他节之间跳转。 43 | 44 | 我们将采取更有组织的方法:每个对象文件的节将与其他对象文件中相同类型的节组合在一起,这个过程称为重定位,这就是为什么对象文件的 ELF 文件类型会标记为“可重定位”。但重定位不仅仅是组装匹配的节,还涉及更新文件内的内部引用,例如:变量、函数、符号表索引和字符串表索引的地址。每个这些值都是其自身对象文件的本地值,从零开始编号。在合并文件时,必须调整这些值,以确保它们引用合并文件中的正确地址。 45 | 46 | 图 8.2 显示了重定位的操作过程 —— .text 节已经重定位,.data 节正在从所有链接的文件中组装,.rodata 和 .strtab 节将遵循相同的流程(简单起见,图中不包含头部): 47 | 48 | \myGraphic{0.9}{content/chapter8/images/2.png}{图 8.2:.data 节的重定位} 49 | 50 | 接下来,链接器需要解析引用。当一个翻译单元的代码引用另一个单元中定义的符号时,无论是通过包含其头文件,还是使用 extern 关键字,编译器都会承认这个声明,并假定稍后会有其他单元提供定义。链接器的主要角色是收集这些未解决的外部符号引用,然后识别并在合并的可执行文件中填充其所属的地址。图 8.3 显示了这个引用解析过程的简单示例: 51 | 52 | \myGraphic{0.9}{content/chapter8/images/3.png}{图 8.3:引用解析} 53 | 54 | 如果开发者不了解这是如何工作的,这部分链接可能会成为问题的来源。我们可能会得到无法找到其对应外部符号的未解决引用,或者相反:提供了太多的定义,而链接器不知道选择哪一个。 55 | 56 | 最终的可执行文件与对象文件非常相似,包含了解决引用的重定位节、节头部表,当然还有描述整个文件的 ELF 头部。主要的区别在于存在程序头部,如下图所示: 57 | 58 | \myGraphic{0.9}{content/chapter8/images/4.png}{图 8.4:ELF 中可执行文件的结构} 59 | 60 | 程序头部位于 ELF 头部之后。操作系统的加载器将读取这个程序头部来设置程序,配置内存布局,并创建进程映像。程序头中的条目指定了按照什么样的顺序复制哪些节,以及将节复制到虚拟内存中的哪些地址中。还包含关于访问控制标志(读、写或执行)的信息,以及一些其他的有用信息。创建的进程中的每个命名节,将由一个内存片段表示,称为段。 61 | 62 | 对象文件也可以打包在库中,库是一个中间产品,可以用于最终的可执行文件或另一个库。 63 | 64 | 现在了解了链接的工作原理,让我们继续下一部分,在那里将讨论三种不同类型的库。 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /book/content/chapter8/3.tex: -------------------------------------------------------------------------------- 1 | 2 | 编译源码后,通常希望避免对同一平台重新编译,或将编译输出与外部项目共享。可以将最初生成的单个对象文件分发出去,但这会带来挑战。分发多个文件,并将它们逐个集成到构建系统中可能很麻烦,特别是在处理大量文件时。更有效的方法是将所有对象文件合并为一个单元以供共享。CMake大大简化了这个任务。可以使用简单的add\_library()命令(与target\_link\_libraries()命令配对)生成这些库。 3 | 4 | 按照约定,所有库都有一个公共前缀(即lib),并使用特定于系统的扩展名,来表示它们是什么类型的库: 5 | 6 | \begin{itemize} 7 | \item 8 | 类Unix系统中,静态库具有.a扩展名,在Windows上为.lib。 9 | 10 | \item 11 | 某些类Unix系统(如Linux)上,共享库(和模块)具有.so扩展名,而在其他系统(如macOS)上为.dylib。在Windows上,扩展名为.dll。 12 | 13 | \item 14 | 共享模块通常使用与共享库相同的扩展名,但不总是如此。在macOS上,可以使用.so,特别是当模块是从另一个Unix平台移植过来时。 15 | \end{itemize} 16 | 17 | 构建库(静态、共享或共享模块)的过程通常称为“链接”,在ch08/01-libraries项目的构建输出中看到: 18 | 19 | \begin{shell} 20 | [ 33%] Linking CXX static library libmy_static.a 21 | [ 66%] Linking CXX shared library libmy_shared.so 22 | [100%] Linking CXX shared module libmy_module.so 23 | [100%] Built target module_gui 24 | \end{shell} 25 | 26 | 然而,前面提到的库并非全部需要使用链接器来创建。对于某些库,该过程可能会跳过某些步骤,如重定位和引用解析。 27 | 28 | \mySubsubsection{8.3.1.}{静态库} 29 | 30 | 静态库本质上是将原始对象文件的集合存储在一起的存档,它们会通过索引来加速链接过程。在类Unix系统中,这样的存档可以通过ar工具创建,并使用ranlib进行索引。 31 | 32 | 构建过程中,只有静态库中必要的符号,才会导入最终的可执行文件中,优化其大小和内存使用。这种选择性集成确保了可执行文件是自包含的,消除了在运行时对外部文件的需求。 33 | 34 | 要创建静态库,可以简单地使用前面章节中已经看到的命令: 35 | 36 | \begin{shell} 37 | add_library(<name> [<source>...]) 38 | \end{shell} 39 | 40 | 这个简写代码默认会生成一个静态库,如果将BUILD\_SHARED\_LIBS变量设置为ON,会默认生成动态库。如果确定想要构建一个静态库,可以显式提供一个关键字: 41 | 42 | \begin{shell} 43 | add_library(<name> STATIC [<source>...]) 44 | \end{shell} 45 | 46 | 使用静态库可能并不总是理想的选择,特别是同一台机器上运行的多个应用程序都使用这些编译的代码时。 47 | 48 | \mySubsubsection{8.3.2.}{共享库} 49 | 50 | 共享库与静态库有显著不同。是使用链接器构建的,链接器完成了链接的两个阶段。这产生了一个包含节头、节和节头表的完整文件,如图8.1所示。 51 | 52 | 共享库,通常称为共享对象,可以在多个不同的应用程序中同时使用。当第一个程序使用共享库时,操作系统会将该库的一个实例加载到内存中。操作系统会为后续使用该库的程序提供相同的地址,这要归功于复杂的虚拟内存机制。然而,对于每个使用该库的进程,库的.data和.bss段是分别实例化的。这确保了每个进程,都可以调整其变量,而不影响其他进程。 53 | 54 | 感谢这种方法,系统整体内存使用得到了优化。如果使用主流认可的库,可能不需要将其包含在程序中,它可能已经存在于目标机器上。但若没有预安装,用户需要在运行应用程序之前手动安装。如果安装的库版本与预期版本不同,这可能导致潜在问题,这类问题称为“依赖地狱”。 55 | 56 | 我们可以通过显式使用SHARED关键字来构建共享库: 57 | 58 | \begin{shell} 59 | add_library(<name> SHARED [<source>...]) 60 | \end{shell} 61 | 62 | 由于共享库在程序初始化期间加载到操作系统的内存中,因此执行程序与磁盘上的实际库文件之间没有关联。相反,链接是间接完成的。类Unix系统中,这是通过共享对象名(SONAME)实现的,可以将其理解为库的“逻辑名称”。 63 | 64 | 这允许在库版本控制上具有灵活性,并确保对库的向后兼容性更改,不会破坏依赖的应用程序。 65 | 66 | 可以使用生成器表达式,查询产生的SONAME文件的一些路径属性(确保将target替换为目标名称): 67 | 68 | \begin{itemize} 69 | \item 70 | \$<TARGET\_SONAME\_FILE:target> 返回完整路径 (.so.3)。 71 | 72 | \item 73 | \$<TARGET\_SONAME\_FILE\_NAME:target> 只返回文件名。 74 | 75 | \item 76 | \$<TARGET\_SONAME\_FILE\_DIR:target> 只返回相应文件夹路径。 77 | \end{itemize} 78 | 79 | 本书后面将介绍的高级场景中非常有用,包括: 80 | 81 | \begin{itemize} 82 | \item 83 | 打包和安装过程中正确使用生成的库。 84 | 85 | \item 86 | 编写自定义的CMake规则进行依赖管理。 87 | 88 | \item 89 | 测试过程中利用SONAME。 90 | 91 | \item 92 | 构建后命令中复制或重命名生成的库。 93 | \end{itemize} 94 | 95 | 可能对其他特定于操作系统的工件有类似的需求,CMake提供了两个生成器表达式,提供了与SONAME相同的后缀。对于Windows: 96 | 97 | \begin{itemize} 98 | \item 99 | \$<TARGET\_LINKER\_FILE:target>返回与生成的动态链接库(DLL)关联的.lib导入库的完整路径。请注意,.lib扩展名与静态Windows库相同,但应用并不相同。 100 | 101 | \item 102 | \$<TARGET\_RUNTIME\_DLLS:target> 返回目标在运行时依赖的DLL列表。 103 | 104 | \item 105 | \$<TARGET\_PDB\_FILE:target> 返回.pdb程序数据库文件的完整路径(用于调试)。 106 | \end{itemize} 107 | 108 | 由于共享库在程序初始化期间加载到操作系统的内存中,因此适用于提前知道程序将使用哪些库的情况。在运行时确定这些的情况又如何呢? 109 | 110 | \mySubsubsection{8.3.3.}{共享模块} 111 | 112 | 共享模块或模块库是共享库的一种变体,设计用于在运行时作为插件加载。与在程序启动时自动加载的标准共享库不同,共享模块仅在程序明确请求时加载。可以通过以下系统调用来完成: 113 | 114 | \begin{itemize} 115 | \item 116 | 在Windows上使用LoadLibrary 117 | 118 | \item 119 | 在Linux和macOS上使用dlopen(),然后是dlsym() 120 | \end{itemize} 121 | 122 | 这种方法的主要原因是节约内存。许多软件应用程序,在整个进程生命周期中都未使用高级功能。每次都将这些功能加载到内存中将非常低效。 123 | 124 | 或者,提供一种途径,以便通过可以单独销售、交付和加载的专业功能来扩展主程序。 125 | 126 | 要构建共享模块,需要使用MODULE关键字: 127 | 128 | \begin{shell} 129 | add_library(<name> MODULE [<source>...]) 130 | \end{shell} 131 | 132 | 模块设计为与将使用它的可执行文件分开部署,所以不应该尝试将模块与可执行文件链接。 133 | 134 | \mySubsubsection{8.3.4.}{位置无关代码(PIC)} 135 | 136 | 由于使用了虚拟内存,程序本质上是某种程度上位置无关的。这项技术抽象了物理地址。当调用一个函数时,CPU使用内存管理单元(MMU)将虚拟地址(每个进程从0开始)转换为相应的物理地址(在分配时确定)。有趣的是,这些映射并不总是遵循特定的顺序。 137 | 138 | 编译库引入了不确定性:不清楚哪些进程可能会使用库,或者在虚拟内存中的位置。无法预测符号的地址,或其相对于库的机器代码的位置。为了处理这个问题,需要另一层间接寻址。 139 | 140 | PIC将符号(如对函数和全局变量的引用)映射到运行时地址。PIC在二进制文件中引入了一个新的节:全局偏移表(GOT)。链接期间,计算了GOT节相对于.text节(程序代码)的相对位置。所有符号引用将通过一个偏移量指向GOT中的占位符。 141 | 142 | 当程序加载时,GOT节转换为一个内存段。随着时间的推移,这个段累积了符号的运行时地址。这种方法称为“延迟加载”,确保加载器仅在需要时填充特定的GOT条目。 143 | 144 | 共享库和模块的所有源代码必须在使用PIC标志激活的情况下编译。通过将POSITION\_INDEPENDENT\_CODE目标属性设置为ON,将告诉CMake适当地添加编译器特定的标志,例如为GCC或Clang添加-fPIC。 145 | 146 | 这个属性对于共享库是自动启用的。如果共享库依赖于另一个目标,例如静态或对象库,也必须将这个属性应用于依赖目标: 147 | 148 | \begin{shell} 149 | set_target_properties(dependency 150 | PROPERTIES POSITION_INDEPENDENT_CODE ON) 151 | \end{shell} 152 | 153 | 因为会检查这个属性的一致性,所以忽略这一步将导致CMake中的冲突。 154 | 155 | 我们的下一个讨论点转向符号。特别是,探讨名称冲突,这可能导致模糊和定义不一致。 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /book/content/chapter8/4.tex: -------------------------------------------------------------------------------- 1 | 2 | Phil Karlton,Netscape的主要怪人和技术愿景家,说下面的话时是正确的: 3 | 4 | \begin{myTip}{Tip} 5 | “计算机科学中有两件难事:缓存失效和给予命名。” 6 | \end{myTip} 7 | 8 | 名称之所以难以处理,有几个原因。它们必须精确而简单,简洁而有表现力。这不仅赋予了它们意义,还使程序员能够掌握底层原始实现背后的概念。C++和许多其他语言增加了另一个规定:大多数名称必须唯一。 9 | 10 | 这一要求以ODR的形式体现:在单个翻译单元(单个.cpp文件)的范围内,即使某个名称(无论是变量、函数、类类型、枚举、概念还是模板)多次声明,也必须精确地定义它一次。"声明"是引入符号,而"定义"则提供其所有细节,如变量的值或函数的主体。 11 | 12 | 链接期间,此规则扩展到整个程序,包含在代码中实际使用的所有非内联函数和变量。考虑以下由三个源文件组成的示例: 13 | 14 | \filename{ch08/02-odr-fail/shared.h} 15 | 16 | \begin{cpp} 17 | int i; 18 | \end{cpp} 19 | 20 | \filename{ch08/02-odr-fail/one.cpp} 21 | 22 | \begin{cpp} 23 | #include <iostream> 24 | #include "shared.h" 25 | 26 | int main() { 27 | std::cout << i << std::endl; 28 | } 29 | \end{cpp} 30 | 31 | \filename{ch08/02-odr-fail/two.cpp} 32 | 33 | \begin{cpp} 34 | #include "shared.h 35 | \end{cpp} 36 | 37 | 还包括一个列表文件: 38 | 39 | \filename{ch08/02-odr-fail/CMakeLists.txt} 40 | 41 | \begin{cmake} 42 | cmake_minimum_required(VERSION 3.26) 43 | project(ODR CXX) 44 | set(CMAKE_CXX_STANDARD 20) 45 | add_executable(odr one.cpp two.cpp) 46 | \end{cmake} 47 | 48 | 这个例子非常简单——创建了一个shared.h头文件,定义了变量i,其在两个单独的翻译单元中使用: 49 | 50 | \begin{itemize} 51 | \item 52 | one.cpp 只是将i打印到屏幕上 53 | 54 | \item 55 | two.cpp 只包含头文件 56 | \end{itemize} 57 | 58 | 但当尝试构建示例时,链接器产生了以下错误: 59 | 60 | \begin{shell} 61 | /usr/bin/ld: 62 | CMakeFiles/odr.dir/two.cpp.o:(.bss+0x0): multiple definition of 'i'; 63 | CMakeFiles/odr.dir/one.cpp.o:(.bss+0x0): first defined here 64 | collect2: error: ld returned 1 exit status 65 | \end{shell} 66 | 67 | 符号不能定义多次,但有一个重要的例外。类型、模板和extern内联函数可以在多个翻译单元中有重复的定义,但前提是这些定义必须完全相同(它们有完全相同的标记序列)。 68 | 69 | 为了证明这一点,用一个类型的定义替换变量的定义: 70 | 71 | \filename{ch08/03-odr-success/shared.h} 72 | 73 | \begin{cpp} 74 | struct shared { 75 | static inline int i = 1; 76 | }; 77 | \end{cpp} 78 | 79 | 然后,这样使用: 80 | 81 | \filename{ch08/03-odr-success/one.cpp} 82 | 83 | \begin{cpp} 84 | #include <iostream> 85 | #include "shared.h" 86 | int main() { 87 | std::cout << shared::i << std::endl; 88 | } 89 | \end{cpp} 90 | 91 | 其他两个文件,two.cpp和CMakeLists.txt,保持与02-odr-fail示例相同。这样的更改会链接成功: 92 | 93 | \begin{shell} 94 | [ 33%] Building CXX object CMakeFiles/odr.dir/one.cpp.o 95 | [ 66%] Building CXX object CMakeFiles/odr.dir/two.cpp.o 96 | [100%] Linking CXX executable odr 97 | [100%] Built target odr 98 | \end{shell} 99 | 100 | 另外,可以将变量标记为翻译单元的局部变量(不会导出到对象文件之外)。为此,将使用static关键字(这个关键字具有上下文相关性,所以不要将它与类中的static关键字混淆): 101 | 102 | \filename{ch08/04-odr-success/shared.h} 103 | 104 | \begin{cpp} 105 | static int i; 106 | \end{cpp} 107 | 108 | 如果尝试链接这个示例,会看到它是有效的,所以静态变量对于每个翻译单元是分开存储的。因此,对一个的修改不会影响另一个。 109 | 110 | ODR规则对于静态库和对象文件完全相同,但在使用共享库构建代码时,情况就没那么明了——让我们来看一下。 111 | 112 | \mySubsubsection{8.4.1.}{解决动态链接中的重复符号问题} 113 | 114 | 动态链接中允许出现重复的符号。以下示例中,我们将创建两个共享库A和B,每个库都有一个duplicated()函数和两个唯一的a()和b()函数: 115 | 116 | \filename{ch08/05-dynamic/a.cpp} 117 | 118 | \begin{cpp} 119 | #include <iostream> 120 | void a() { 121 | std::cout << "A" << std::endl; 122 | } 123 | void duplicated() { 124 | std::cout << "duplicated A" << std::endl; 125 | } 126 | \end{cpp} 127 | 128 | 第二个实现文件几乎与第一个完全相同: 129 | 130 | \filename{ch08/05-dynamic/b.cpp} 131 | 132 | \begin{cpp} 133 | #include <iostream> 134 | void b() { 135 | std::cout << "B" << std::endl; 136 | } 137 | void duplicated() { 138 | std::cout << "duplicated B" << std::endl; 139 | } 140 | \end{cpp} 141 | 142 | 现在,使用每个函数来看看会发生什么(简单起见,将本地声明为extern): 143 | 144 | \filename{ch08/05-dynamic/main.cpp} 145 | 146 | \begin{cpp} 147 | extern void a(); 148 | extern void b(); 149 | extern void duplicated(); 150 | int main() { 151 | a(); 152 | b(); 153 | duplicated(); 154 | } 155 | \end{cpp} 156 | 157 | 前面的代码将运行每个库中的唯一函数,然后调用在两个动态库中定义的具有相同签名的函数。你认为会发生什么?链接顺序会有关系吗?让我们针对以下两种情况进行测试: 158 | 159 | \begin{itemize} 160 | \item 161 | main\_1 目标将首先与a库链接 162 | 163 | \item 164 | main\_2 目标将首先与b库链接 165 | \end{itemize} 166 | 167 | 列表文件如下所示: 168 | 169 | \filename{ch08/05-dynamic/CMakeLists.txt} 170 | 171 | \begin{cmake} 172 | cmake_minimum_required(VERSION 3.26) 173 | project(Dynamic CXX) 174 | add_library(a SHARED a.cpp) 175 | add_library(b SHARED b.cpp) 176 | add_executable(main_1 main.cpp) 177 | target_link_libraries(main_1 a b) 178 | add_executable(main_2 main.cpp) 179 | target_link_libraries(main_2 b a) 180 | \end{cmake} 181 | 182 | 构建并运行两个可执行文件后,将看到以下输出: 183 | 184 | \begin{shell} 185 | root@ce492a7cd64b:/root/examples/ch08/05-dynamic# b/main_1 186 | A 187 | B 188 | duplicated A 189 | 190 | root@ce492a7cd64b:/root/examples/ch08/05-dynamic# b/main_2 191 | A 192 | B 193 | duplicated B 194 | \end{shell} 195 | 196 | 啊哈!显然,链接库的顺序对链接器有关系。如果不警惕,这可能会导致混淆。与人们可能认为的不同,命名冲突在实践中并不少见。 197 | 198 | 如果定义了本地可见的符号,其将优先于从DLL中可用的符号。在main.cpp中定义duplicated()函数将覆盖两个目标的行为。 199 | 200 | 在从库中导出名称时,总是要非常小心,因为迟早会遇到命名冲突。 201 | 202 | \mySubsubsection{8.4.2.}{使用命名空间——不要依赖链接器} 203 | 204 | C++命名空间是为了避免此类奇怪的问题,并更有效地处理ODR而发明的。最佳实践是将你的库代码包装在以库命名的命名空间中。 205 | 206 | 这种策略有助于防止由于重复符号引起的复杂问题。项目中,我们可能会遇到一个共享库链接到另一个库的情况,形成一个长链。在复杂的配置中,这种情况很常见。然而,理解仅仅将一个库链接到另一个库,并不会引入任何类型的命名空间继承至关重要。这个链中的每个链接的符号都保留在编译时的原始命名空间中。 207 | 208 | 虽然链接器的复杂性很吸引人,偶尔也是必要的,但另一个紧迫的问题经常出现:正确定义的符号会神秘消失。让我们在下一节中深入探讨这个问题。 209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /book/content/chapter8/5.tex: -------------------------------------------------------------------------------- 1 | 2 | 链接器的行为有时可能显得反复无常,无缘无故地抛出错误。这对于不熟悉这个工具复杂性的初级开发者来说,常常是一个特别棘手的挑战。他们通常会尽可能长时间地避免处理构建配置,但当需要做出更改时——也许是要整合他们开发的库——问题就会全面爆发。 3 | 4 | 考虑以下情况:一个相对直接的依赖链,其中主可执行文件依赖于一个“外部”库。而这个外部库又依赖于包含必要变量int b的“嵌套”库。突然之间,一个神秘的错误信息出现在程序员面前: 5 | 6 | \begin{shell} 7 | outer.cpp:(.text+0x1f): undefined reference to 'b' 8 | \end{shell} 9 | 10 | 此类错误很常见。通常,表示链接器中忘记了一个库。然而,在这个场景中,库似乎已经正确地添加到了target\_link\_libraries()命令中: 11 | 12 | \filename{ch08/06-unresolved/CMakeLists.txt} 13 | 14 | \begin{cmake} 15 | cmake_minimum_required(VERSION 3.26) 16 | project(Order CXX) 17 | add_library(outer outer.cpp) 18 | add_library(nested nested.cpp) 19 | add_executable(main main.cpp) 20 | target_link_libraries(main nested outer) 21 | \end{cmake} 22 | 23 | 问题是什么呢!?很少有错误能像这样调试和理解时让人如此愤怒。我们看到的是链接顺序不正确: 24 | 25 | \filename{ch08/06-unresolved/main.cpp} 26 | 27 | \begin{cmake} 28 | #include <iostream> 29 | extern int a; 30 | int main() { 31 | std::cout << a << std::endl; 32 | } 33 | \end{cmake} 34 | 35 | 代码看起来很简单——将打印外部变量a,该变量可以在outer库中找到。提前使用extern关键字声明,以下是该库的源码: 36 | 37 | \filename{ch08/06-unresolved/outer.cpp} 38 | 39 | \begin{cmake} 40 | extern int b; 41 | int a = b; 42 | \end{cmake} 43 | 44 | 这相当简单——outer依赖于嵌套库提供外部变量b,该变量赋值给变量a。来看看nested的源代码,以确认没有遗漏定义: 45 | 46 | \filename{ch08/06-unresolved/nested.cpp} 47 | 48 | \begin{cmake} 49 | int b = 123; 50 | \end{cmake} 51 | 52 | 确实,我们为b提供了定义,并且由于它没有使用static关键字标记为局部变量,因此会从nested目标正确导出。如我们之前所见,此目标在CMakeLists.txt中与主可执行文件链接: 53 | 54 | \begin{cmake} 55 | target_link_libraries(main nested outer) 56 | \end{cmake} 57 | 58 | 那么,对’b’的未定义引用错误从何而来?解析未定义的符号是这样工作的——链接器从左到右处理二进制文件。 59 | 60 | 当链接器遍历二进制文件时,将执行以下操作: 61 | 62 | \begin{enumerate} 63 | \item 64 | 收集此二进制文件导出的所有未定义符号,并存储起来以备后用。 65 | 66 | \item 67 | 尝试用此二进制文件中定义的符号解析未定义的符号(来自迄今为止处理的所有二进制文件)。 68 | 69 | \item 70 | 对下一个二进制文件重复此过程。 71 | \end{enumerate} 72 | 73 | 如果在整个操作完成后仍有符号未定义,链接将失败。这就是我们示例中的情况(CMake将可执行文件目标的对象文件放在库的前面): 74 | 75 | \begin{enumerate} 76 | \item 77 | 链接器处理了main.o,发现对a变量的未定义引用,并将其收集起来以备将来解析。 78 | 79 | \item 80 | 链接器处理了libnested.a,没有发现未定义的引用,也没有需要解析的。 81 | 82 | \item 83 | 链接器处理了libouter.a,发现对b变量的未定义引用,并解析了对a变量的引用。 84 | \end{enumerate} 85 | 86 | 我们正确地解析了对a变量的引用,但未解析对b变量的引用。要纠正这一点,我们需要反转链接顺序,使nested在outer之后: 87 | 88 | \begin{cmake} 89 | arget_link_libraries(main outer nested) 90 | \end{cmake} 91 | 92 | 有时,会遇到循环引用,其中翻译单元相互定义符号,没有一种有效的顺序可以满足所有引用。解决这个问题的唯一方法是对某些目标进行两次处理: 93 | 94 | \begin{cmake} 95 | target_link_libraries(main nested outer nested) 96 | \end{cmake} 97 | 98 | 这是一种常见做法,但使用起来略显不雅。如果有幸使用CMake 3.24或更高版本,可以使用\$<LINK\_GROUP>生成器表达式与RESCAN功能,该功能添加了链接器特定的标志,如-{}-start-group或-{}-end-group,以确保能找到所有的符号: 99 | 100 | \begin{cmake} 101 | target_link_libraries(main "$<LINK_GROUP:RESCAN,nested,outer>") 102 | \end{cmake} 103 | 104 | 这种机制引入了额外的处理步骤,应当只在必要时使用。需要循环引用的情况非常罕见(并且是合理的)。遇到这个问题通常表明设计不佳,其在Linux、BSD、SunOS,以及使用GNU工具链的Windows上得到支持。 105 | 106 | 现在准备处理ODR问题。我们还可能遇到哪些其他问题?链接过程中神秘地缺失符号。来看看这是关于什么的。 107 | 108 | \mySubsubsection{8.5.1.}{处理未引用的符号} 109 | 110 | 当创建库时,尤其是静态库,基本上是由多个对象文件捆绑在一起的归档文件。我们提到过,一些归档工具可能会创建符号索引以加速链接过程。这些索引提供了每个符号与定义它们的对象文件之间的映射。当解析一个符号时,包含该符号的对象文件会合并到最终的二进制文件中(一些链接器通过只包含文件的具体部分来进一步优化)。如果静态库中没有从对象文件引用符号,则可能会完全忽略该对象文件。因此,只有实际使用的静态库部分,才会出现在最终的二进制文件中。 111 | 112 | 然而,在某些情况下一些未引用的符号也需要包含进二进制文件: 113 | 114 | \begin{itemize} 115 | \item 116 | 静态初始化:如果库有需要在main()之前初始化的全局对象(即构造函数需要执行),并且这些对象在其它地方没有直接引用;链接器可能会将其从最终二进制文件中排除。 117 | 118 | \item 119 | 插件架构:如果正在开发一个插件系统(带有模块库),其中代码需要在运行时进行识别和加载,而不需要直接引用。 120 | 121 | \item 122 | 静态库中的未使用代码:如果正在开发一个包含实用函数或代码的静态库,这些代码并非总是直接引用,但仍然希望它们出现在最终二进制文件中。 123 | 124 | \item 125 | 模板实例化:对于重度依赖模板的库;如果未明确提及,一些模板实例化可能会在链接过程中忽略。 126 | 127 | \item 128 | 链接问题:特别是对于复杂的构建系统或详尽的代码库,链接可能会产生不可预测的结果,其中某些符号或代码部分似乎缺失。 129 | \end{itemize} 130 | 131 | 这时,强制在链接过程中包含所有对象文件很有用,可通过一种称为“全归档链接”的模式来实现。 132 | 133 | 特定的编译器链接标志如下: 134 | 135 | \begin{itemize} 136 | \item 137 | GCC使用 -{}-whole-archive 138 | 139 | \item 140 | Clang使用 -{}-force-load 141 | 142 | \item 143 | MSVC使用 /WHOLEARCHIVE 144 | \end{itemize} 145 | 146 | 为了实现这一点,可以使用target\_link\_options()命令: 147 | 148 | \begin{cmake} 149 | target_link_options(tgt INTERFACE 150 | -Wl,--whole-archive $<TARGET_FILE:lib1> -Wl,--no-whole-archive 151 | ) 152 | \end{cmake} 153 | 154 | 然而,这个命令是特定于链接器的,因此结合生成器表达式,来检测不同的编译器,并提供必要的标志。幸运的是,CMake 3.24引入了一个新的生成器表达式用于此目的: 155 | 156 | \begin{cmake} 157 | target_link_libraries(tgt INTERFACE 158 | "$<LINK_LIBRARY:WHOLE_ARCHIVE,lib1>" 159 | ) 160 | \end{cmake} 161 | 162 | 使用这种方法可以确保tgt目标包含lib1库的所有对象文件。 163 | 164 | 尽管如此,还需要考虑一些潜在的问题: 165 | 166 | \begin{itemize} 167 | \item 168 | 增加二进制文件大小:这个标志可能会大幅增加最终二进制文件大小,其包含了指定库的所有对象文件,无论是否使用。 169 | 170 | \item 171 | 符号冲突的可能性:引入所有符号可能会导致与其他符号冲突,从而引发链接错误。 172 | 173 | \item 174 | 维护负担:过度依赖此类标志会掩盖代码设计或结构中的潜在问题。 175 | \end{itemize} 176 | 177 | 了解了如何解决常见的链接挑战后,我们现在可以继续准备项目的测试了。 178 | 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /book/content/chapter8/6.tex: -------------------------------------------------------------------------------- 1 | 链接器已经强制执行ODR,并确保在链接过程中所有外部符号,都提供定义。另一个需要面临的与链接器相关的挑战是,对项目进行优雅且高效的测试。 2 | 3 | 理想情况下,应该测试与生产环境中运行完全相同的源代码。全面的测试管道会构建源代码,对生成的二进制文件运行测试,然后打包和分发可执行文件(可选地排除测试本身)。 4 | 5 | 但是,如何实现呢?可执行文件通常具有精确的执行流程,通常涉及读取命令行参数。C++的编译特性并不容易支持可临时插入二进制文件中,进行测试的可插拔单元。这表明我们需要一个微妙的策略来应对这个挑战。 6 | 7 | 幸运的是,可以使用链接器,以优雅的方式来处理这个问题。考虑将程序的所有逻辑从main()函数中提取出来,放到一个外部函数start\_program()中: 8 | 9 | \filename{ch08/07-testing/main.cpp} 10 | 11 | \begin{cpp} 12 | extern int start_program(int, const char**); 13 | int main(int argc, const char** argv) { 14 | return start_program(argc, argv); 15 | } 16 | \end{cpp} 17 | 18 | 当main()函数以这种形式编写时,跳过对其的测试是合理的;只是将参数转发到一个在别处定义的函数(在另一个文件中)。然后,可以创建一个包含原始main()源代码的新函数start\_program()的库。这个例子中,代码检查命令行参数计数是否大于1: 19 | 20 | \filename{ch08/07-testing/program.cpp} 21 | 22 | \begin{cpp} 23 | #include <iostream> 24 | int start_program(int argc, const char** argv) { 25 | if (argc <= 1) { 26 | std::cout << "Not enough arguments" << std::endl; 27 | return 1; 28 | } 29 | return 0; 30 | } 31 | \end{cpp} 32 | 33 | 现在,可以准备一个项目来构建这个应用程序,并将这两个翻译单元链接在一起: 34 | 35 | \filename{ch08/07-testing/CMakeLists.txt} 36 | 37 | \begin{cmake} 38 | cmake_minimum_required(VERSION 3.26) 39 | project(Testing CXX) 40 | add_library(program program.cpp) 41 | add_executable(main main.cpp) 42 | target_link_libraries(main program) 43 | \end{cmake} 44 | 45 | main目标只提供了所需的main()函数,命令行参数验证逻辑包含在program目标中。现在可以通过创建另一个带有main()函数的可执行文件来进行测试,该文件将作为测试用例。 46 | 47 | 在现实世界的场景中,像GoogleTest或Catch2这样的框架将提供自己的main()方法,可以用来替换程序的入口点并运行所有定义的测试。我们将在第11章中深入讨论实际的测试主题。现在,先来关注一般性问题,并在main()函数中直接编写测试用例: 48 | 49 | \filename{ch08/07-testing/test.cpp} 50 | 51 | \begin{cpp} 52 | #include <iostream> 53 | extern int start_program(int, const char**); 54 | using namespace std; 55 | int main() 56 | { 57 | cout << "Test 1: Passing zero arguments to start_program:\n"; 58 | auto exit_code = start_program(0, nullptr); 59 | if (exit_code == 0) 60 | cout << "Test FAILED: Unexpected zero exit code.\n"; 61 | else 62 | cout << "Test PASSED: Non-zero exit code returned.\n"; 63 | cout << endl; 64 | 65 | cout << "Test 2: Passing 2 arguments to start_program:\n"; 66 | const char *arguments[2] = {"hello", "world"}; 67 | exit_code = start_program(2, arguments); 68 | if (exit_code != 0) 69 | cout << "Test FAILED: Unexpected non-zero exit code\n"; 70 | else 71 | cout << "Test PASSED\n"; 72 | } 73 | \end{cpp} 74 | 75 | 前面的代码将两次调用start\_program,一次不带参数,一次带参数,并检查返回的退出代码是否正确。如果测试正确执行,将看到以下输出: 76 | 77 | \begin{shell} 78 | ./test 79 | Test 1: Passing zero arguments to start_program: 80 | Not enough arguments 81 | Test PASSED: Non-zero exit code returned 82 | 83 | Test 2: Passing 2 arguments to start_program: 84 | Test PASSED 85 | \end{shell} 86 | 87 | “Not enough arguments”这一行来自start\_program(),并且是预期的错误消息(正在检查程序是否正确地失败)。 88 | 89 | 这个单元测试在干净代码和优雅测试实践方面还有很多不足,但这是一个开始。 90 | 91 | 现在,已经两次定义了main(): 92 | 93 | \begin{itemize} 94 | \item 95 | main.cpp 用于生产环境 96 | 97 | \item 98 | test.cpp 用于测试目的 99 | \end{itemize} 100 | 101 | 现在,在CMakeLists.txt的底部定义测试可执行文件: 102 | 103 | \begin{cmake} 104 | add_executable(test test.cpp) 105 | target_link_libraries(test program) 106 | \end{cmake} 107 | 108 | 这个添加创建了一个新的目标,其与我们的生产代码链接相同的二进制代码。然而,这给予了我们灵活性,可以按需调用所有导出的函数。多亏了这个,可以自动运行所有代码路径,并检查它们是否按预期工作。太棒了! 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /book/content/chapter8/7.tex: -------------------------------------------------------------------------------- 1 | 在 CMake 中进行链接可能一开始看起来很简单,但当我们深入挖掘时,会发现其背后大有乾坤。毕竟,链接可执行文件并不像拼图那样简单。当深入研究对象文件和库的结构时,存储各种类型的数据、指令、符号名称等节(section)都需要重新排序。在程序运行之前,这些节需要经过重定位。 2 | 3 | 解析符号也非常关键。链接器必须对所有翻译单元中的引用进行排序,确保没有遗漏。当这些都处理好了,链接器就会创建程序头部,并将其放入最终的可执行文件中。这个头部为系统加载器提供指令,详细说明如何将合并的节转换为段,这些段将构成进程的运行时内存镜像。我们还讨论了三种类型的库:静态库、共享库和共享模块。探讨了它们的区别,以及在哪些场景下某种库可能比其他库更适合使用。此外,还提到了 PIC —— 一个强大的概念,促进了符号的延迟绑定。 4 | 5 | ODR 是 C++ 的一个概念,由链接器强制执行。我们查看了如何在静态库和动态库中处理最基本的符号重复问题。还强调了尽可能使用命名空间的价值,并建议不要过分依赖链接器来避免符号冲突。 6 | 7 | 对于一个看似简单的步骤(考虑到 CMake 专门用于链接的有限命令),其确实有其复杂性。特别是在处理具有嵌套和循环依赖关系的库时,其中一个比较棘手的问题是链接的顺序。现在我们了解了链接器如何选择最终进入二进制文件的符号,以及如何在需要时覆盖这种行为。 8 | 9 | 最后,研究了如何利用链接器为测试准备程序 —— 通过将 main() 函数分离到另一个翻译单元。这使我们能够引入另一个可执行文件,该文件针对与生产中将要执行的确切机器代码运行测试。 10 | 11 | 凭借对链接的新知识,已准备好将外部库引入 CMake 项目。下一章中,将探讨如何在 CMake 中管理依赖项。 -------------------------------------------------------------------------------- /book/content/chapter8/8.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | \begin{itemize} 4 | \item 5 | 可执行与链接格式 (ELF) 文件结构: 6 | 7 | \url{https://en.wikipedia.org/wiki/Executable_and_Linkable_Format} 8 | 9 | \item 10 | CMake 手册关于 add\_library() 的用法: 11 | 12 | \url{https://cmake.org/cmake/help/latest/command/add_library.html} 13 | 14 | \item 15 | 依赖地狱(dependency hell): 16 | 17 | \url{https://en.wikipedia.org/wiki/Dependency_hell} 18 | 19 | \item 20 | 模块与共享库之间的区别: 21 | 22 | \url{https://stackoverflow.com/questions/4845984/difference-between-modulesand-shared-libraries} 23 | \end{itemize} -------------------------------------------------------------------------------- /book/content/chapter8/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter8/images/1.png -------------------------------------------------------------------------------- /book/content/chapter8/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter8/images/2.png -------------------------------------------------------------------------------- /book/content/chapter8/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter8/images/3.png -------------------------------------------------------------------------------- /book/content/chapter8/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter8/images/4.png -------------------------------------------------------------------------------- /book/content/chapter9/0.tex: -------------------------------------------------------------------------------- 1 | 解决方案的大小起始并不重要;但随着它的增长,可能会选择依赖其他项目。避免创建和维护样板代码的工作至关重要,可以为真正重要的事情腾出时间:业务逻辑。外部依赖项有多种用途,并提供框架和功能,解决复杂问题,在构建和确保代码质量中发挥关键作用。这些依赖项各不相同,从专业的编译器如Protocol Buffers(Protobuf)到测试框架如Google Test。 2 | 3 | 使用开源项目或内部代码时,高效地管理外部依赖项是必不可少的。如果手动完成这项工作,将需要大量的设置时间和持续的支持。幸运的是,CMake擅长处理各种依赖管理方法,同时紧跟行业标准。 4 | 5 | 首先将了解如何识别和利用宿主系统上已经存在的依赖项,从而避免不必要的下载和延长的编译时间。这项任务相对简单,许多软件包要么与CMake兼容,要么直接由CMake支持。我们还将探讨如何指导CMake定位和包含那些缺乏这种本地支持的依赖项。对于历史遗留的软件包,采用另一种方法可能更有益:可以使用曾经流行的pkg-config工具来处理更繁琐的任务。 6 | 7 | 此外,还将深入探讨如何管理在线可用,但尚未安装在系统上的依赖项。我们将研究如何从HTTP服务器、Git和其他类型的仓库中获取这些依赖项。我们还将讨论如何选择最佳方法:首先在系统中搜索,如果未找到该软件包,再转而获取。最后,将回顾一种在特殊情况下可能适用的下载外部项目的旧技术。 8 | 9 | 本章,将包含以下内容: 10 | 11 | \begin{itemize} 12 | \item 13 | 使用已安装的依赖项 14 | 15 | \item 16 | 使用系统中不存在的依赖项 17 | \end{itemize} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /book/content/chapter9/1.tex: -------------------------------------------------------------------------------- 1 | 可以在GitHub上找到本章中出现的代码文件,地址为 \url{https://github.com/PacktPublishing/Modern-CMake-for-Cpp-2E/tree/main/examples/ch09}。 2 | 3 | 为了构建本书提供的示例,请使用以下推荐的命令: 4 | 5 | \begin{shell} 6 | cmake -B <build tree> -S <source tree> 7 | cmake --build <build tree> 8 | \end{shell} 9 | 10 | 请确保将<build tree>和<source tree>占位符替换为适当的路径。 提醒一下:<build tree>是目标/输出目录的路径,而<source tree>是源码所在的位置。 -------------------------------------------------------------------------------- /book/content/chapter9/4.tex: -------------------------------------------------------------------------------- 1 | 本章为提供了使用CMake的查找模块识别系统安装的软件包的知识,以及如何利用随库提供的配置文件。对于不支持CMake但包含.pc文件的旧库,可以使用PkgConfig工具和CMake内置的FindPkgConfig查找模块。 2 | 3 | 还探讨了FetchContent模块的功能。这个模块允许在配置CMake时从各种来源下载依赖项,同时首先扫描系统,从而避免不必要的下载。我们提到了这些模块的历史背景,并讨论了在特殊情况下使用ExternalProject模块的选项。CMake设计为通过讨论的方法定位库时自动生成构建目标,这为过程增加了一层便利和优雅。 4 | 5 | 有了这个基础,就可以将标准库整合到项目中了。 6 | 7 | 下一章中,我们将学习如何使用C++20模块,在较小规模上提供可重用的代码。 -------------------------------------------------------------------------------- /book/content/chapter9/5.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | \begin{itemize} 4 | \item 5 | CMake文档 - 提供的查找模块: 6 | 7 | \url{https://cmake.org/cmake/help/latest/manual/cmake-modules.7.html#find modules} 8 | 9 | \item 10 | CMake文档 - 使用依赖项指南: 11 | 12 | \url{https://cmake.org/cmake/help/latest/guide/using-dependencies/index.html} 13 | 14 | \item 15 | CMake和使用git-submodule管理依赖项目: 16 | 17 | \url{https://stackoverflow.com/questions/43761594/} 18 | 19 | \item 20 | 利用PkgConfig: 21 | 22 | \url{https://gitlab.kitware.com/cmake/community/-/wikis/doc/tutorials/How-To-Find-Libraries#piggybacking-on-pkg-config} 23 | 24 | \item 25 | 如何使用ExternalProject: 26 | 27 | \url{https://www.jwlawson.co.uk/interest/2020/02/23/cmake-external-project.html} 28 | 29 | \item 30 | CMake FetchContent与ExternalProject的比较: 31 | 32 | \url{https://www.scivision.dev/cmake-fetchcontent-vs-external-project/} 33 | 34 | \item 35 | 使用CMake与外部项目: 36 | 37 | \url{http://www.saoe.net/blog/using-cmake-with-external-projects/} 38 | \end{itemize} 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /book/content/chapter9/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/book/content/chapter9/images/1.png -------------------------------------------------------------------------------- /cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CMake-for-Cpp-2ed/9b1fa1abbe54b17fe0d77fbc9198b1f497dadc4d/cover.png --------------------------------------------------------------------------------