第二十一章:指定版本详细信息

Posted by lili on

版本控制经常是容易被忽视的事情之一。版本号向用户传达的信息的重要性经常被低估,导致用户期望未达到或者对版本发布之间的变化感到困惑。此外,营销和版本控制策略对构建、打包等技术实现之间不可避免的紧张关系也存在。在早期考虑并建立这些事项可以使项目在首次发布时处于更好的位置。本章探讨了实现有效版本控制策略的方法,利用CMake功能提供强大、高效的流程。

21.1. 项目版本

项目版本通常需要在顶层CMakeLists.txt文件的开头附近定义,以便构建的各个部分可以引用它。源代码可能希望嵌入项目版本,以便向用户显示或记录在日志文件中,打包步骤可能需要它来定义发布版本的详细信息等。可以简单地在CMakeLists.txt文件的开头附近设置一个变量来记录所需形式的版本号,如下所示:

cmake_minimum_required(VERSION 3.0)
project(FooBar)
set(FooBar_VERSION 2.4.7)

如果需要提取单独的组件,则可能需要定义一组稍微复杂的变量。一个示例可能如下所示:

cmake_minimum_required(VERSION 3.0)
project(FooBar)
set(FooBar_VERSION_MAJOR 2)
set(FooBar_VERSION_MINOR 4)
set(FooBar_VERSION_PATCH 7)
set(FooBar_VERSION
 ${FooBar_VERSION_MAJOR}.${FooBar_VERSION_MINOR}.${FooBar_VERSION_PATCH}
)

不同的项目可能使用不同的变量命名约定。版本号的结构也可能因项目而异,由此产生的不一致性使得将许多项目作为较大集合或超级构建的一部分更加困难(见34.1节,“超级构建结构”)。

CMake 3.0引入了新功能,使得指定版本详细信息更加容易,并为项目版本编号带来了一些一致性。VERSION关键字被添加到project()命令中,要求版本号的格式为major.minor.patch.tweak。从这些信息中,一组变量被自动填充,以便将完整版本字符串以及每个版本组件单独提供给项目的其余部分。如果提供的版本字符串中有一些部分被省略(例如通常省略了tweak部分),则相应的变量将为空。

当使用VERSION关键字与project()命令时,以下表格显示了自动填充的版本变量:

PROJECT_VERSION projectName_VERSION
PROJECT_VERSION_MAJOR projectName_VERSION_MAJOR
PROJECT_VERSION_MINOR projectName_VERSION_MINOR
PROJECT_VERSION_PATCH projectName_VERSION_PATCH
PROJECT_VERSION_TWEAK projectName_VERSION_TWEAK

这两组变量稍微有些不同。项目特定的projectName_…变量可以在当前目录范围或以下任何位置获取版本详细信息。例如,像project(FooBar VERSION 2.7.3)这样的调用会导致变量命名为FooBar_VERSION,FooBar_VERSION_MAJOR等。由于对project()的两次调用不能使用相同的projectName,这些项目特定变量不会被其他对project()命令的调用覆盖。另一方面,PROJECT_…变量在每次调用project()时更新,因此它们可以用于提供当前范围或以上对project()最近一次调用的版本详细信息。

从CMake 3.12开始,还提供了一组类似的变量,用于在顶层CMakeLists.txt文件中由project()调用设置的版本详细信息。这些变量是:

  • CMAKE_PROJECT_VERSION
  • CMAKE_PROJECT_VERSION_MAJOR
  • CMAKE_PROJECT_VERSION_MINOR
  • CMAKE_PROJECT_VERSION_PATCH
  • CMAKE_PROJECT_VERSION_TWEAK

同样的模式也被用来为项目名称、描述和主页URL提供变量,后两者分别在CMake版本3.9和3.12中添加。作为一般指南,PROJECT_…变量可以作为通用代码(特别是模块)的有用方式,用来定义诸如打包或文档详情之类的合理默认值。CMAKE_PROJECT_…变量有时也用于默认值,但它们可能稍微不太可靠,因为它们的使用通常假设一个特定的顶层项目。projectName_…变量是最可靠的,因为它们在提供哪个项目的详细信息方面始终是明确的。

在处理支持CMake版本早于3.0的项目时,有时会出现它们定义的版本相关变量与CMake 3.0及以后版本自动定义的变量冲突的情况。这可能导致CMP0048策略警告,突出显示冲突。以下是导致此类警告的代码示例:

cmake_minimum_required(VERSIONset(FooBar_VERSION 2.4.7)
project(FooBar)

在上面的示例中,明确设置了FooBar_VERSION变量,但这个变量名与project()命令自动定义的变量发生了冲突。由此产生的策略警告旨在鼓励项目要么使用不同的变量名,要么升级到至少CMake版本3.0,并在project()命令中设置版本详情。

21.2. 源代码访问版本详细信息

一旦在CMakeLists.txt文件中定义了版本详细信息,非常常见的需求是使它们可供项目编译的源代码使用。可以使用多种不同的方法,每种方法都有其优点和缺点。对于那些刚接触CMake的人来说,最常见的技术之一是在项目的顶层添加编译器定义:

cmake_minimum_required(VERSION 3.0)
project(FooBar VERSION 2.4.7)
add_definitions(-DFOOBAR_VERSION=\"${FooBar_VERSION}\")

这将版本作为原始字符串提供,可以像这样使用:

void printVersion()
{
 
 std::cout << FOOBAR_VERSION << std::endl;
}

虽然这种方法相当简单,但将定义添加到项目中的每个文件的编译中也带来了一些缺点。除了使每个要编译的文件的命令行混乱之外,这意味着每次版本号更改时都会重新构建整个项目。这可能看起来是一个小问题,但是在源代码控制系统中经常在不同分支之间切换的开发人员几乎肯定会对所有不必要的重新编译感到非常恼火。稍微更好的方法是使用源属性仅为需要的文件定义FOOBAR_VERSION符号。例如:

cmake_minimum_required(VERSION 3.0)
project(FooBar VERSION 2.4.7)
add_executable(FooBar main.cpp src1.cpp src2.cpp ...)
get_source_file_property(defs src1.cpp COMPILE_DEFINITIONS)
list(APPEND defs "FOOBAR_VERSION=\"${FooBar_VERSION}\"")
set_source_files_properties(src1.cpp PROPERTIES
 COMPILE_DEFINITIONS "${defs}"
)

这避免了将编译器定义添加到每个文件中,而是仅将其添加到需要的文件中。然而,正如在第9.5节“源属性”中提到的,当设置单个源属性时可能会对构建依赖产生负面影响,这再次导致了比必要的更多的文件重新构建。因此,这种方法看起来可能是一个改进,但通常情况下并非如此。

与通过命令行传递版本详细信息不同,另一种常见的方法是使用configure_file()编写一个提供版本详细信息的头文件。例如:

foobar_version.h.in

#include <string>
inline std::string getFooBarVersion()
{
 
 return "@FooBar_VERSION@";
}
inline unsigned getFooBarVersionMajor()
{
 
 return @FooBar_VERSION_MAJOR@;
}
inline unsigned getFooBarVersionMinor()
{
 
 return @FooBar_VERSION_MINOR@ +0;
}
inline unsigned getFooBarVersionPatch()
{
 
 return @FooBar_VERSION_PATCH@ +0;
}
inline unsigned getFooBarVersionTweak()
{
 
 return @FooBar_VERSION_TWEAK@ +0;
}

main.cpp:

#include#include"foobar_version.h"
<iostream>
int main(int argc, char* argv[])
{
 
 std::cout << "VERSION = " << getFooBarVersion() << "\n"
 	   << "MAJOR = " << getFooBarVersionMajor() << "\n"
 	   << "MINOR = " << getFooBarVersionMinor() << "\n"
 	   << "PATCH = " << getFooBarVersionPatch() << "\n"
 	   << "TWEAK = " << getFooBarVersionTweak()  << std::endl;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.0)
project(FooBar VERSION 2.4.7)
configure_file(foobar_version.h.in foobar_version.h @ONLY)
add_executable(FooBar main.cpp)
target_include_directories(FooBar PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")

在foobar_version.h.in中的+0是必需的,用于允许在省略这些版本组件的情况下它们对应的变量为空。

通过这样的头文件提供版本详细信息是对以前技术的改进。版本详细信息不包含在任何源文件的编译命令行中,只有包含foobar_version.h头文件的文件在版本详细信息更改时才会重新编译。提供所有不同的版本组件而不仅仅是版本字符串也不会影响命令行。然而,如果许多不同的源文件需要版本号,这仍然可能导致更多的重新编译比实际上是必要的。

可以通过将实现移出头文件并将其编译为自己的.cpp文件来进一步完善这种方法,并将其编译为自己的库。

foobar_version.h

#include <string>
std::string
unsigned
unsigned
unsigned
unsigned
getFooBarVersion();
getFooBarVersionMajor();
getFooBarVersionMinor();
getFooBarVersionPatch();
getFooBarVersionTweak();

foobar_version.cpp.in

#include "foobar_version.h"
std::string getFooBarVersion()
{
 
 return "@FooBar_VERSION@";
}
unsigned getFooBarVersionMajor()
{
 
 return @FooBar_VERSION_MAJOR@;
}
unsigned getFooBarVersionMinor()
{
 
 return @FooBar_VERSION_MINOR@ +0;
}
unsigned getFooBarVersionPatch()
{
 
 return @FooBar_VERSION_PATCH@ +0;
}
unsigned getFooBarVersionTweak()
{
 
 return @FooBar_VERSION_TWEAK@ +0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.0)
project(FooBar VERSION 2.4.7)
configure_file(foobar_version.cpp.in foobar_version.cpp @ONLY)
add_library(FooBar_version STATIC ${CMAKE_CURRENT_BINARY_DIR}/foobar_version.cpp)
target_include_directories(FooBar_version PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
add_executable(FooBar main.cpp)
target_link_libraries(FooBar PRIVATE FooBar_version)
add_library(FooToolkit mylib.cpp)
target_link_libraries(FooToolkit PRIVATE FooBar_version)

这种安排没有以前方法的缺点。当版本详细信息更改时,只需要重新编译一个源文件(生成的foobar_version.cpp文件),并且FooBar和FooToolkit目标只需要重新链接。foobar_version.h头文件永远不会更改,因此任何依赖它的文件在版本详细信息更改时都不会过时。任何源文件的编译命令行也不会添加任何选项,因此不会因为更改版本详细信息而触发其他重新编译。

在项目提供库和头文件作为发布包的情况下,上述安排也是稳健的。头文件不包含版本详细信息,库包含版本详细信息。因此,使用库的代码可以调用版本函数,并确信它们收到的是库构建时的详细信息。这在复杂的最终用户环境中可能会有所帮助,在这些环境中可能安装了项目的多个版本,并且不一定按照项目的意图进行组织。

这种方法的一个变体是将FooBar_version作为对象库而不是静态库。最终结果差不多是一样的,但并没有太多的收益,而且对一些开发人员来说可能会感觉不太自然。将其作为共享库会失去一些稳健性优势,并且再次引入了一点复杂性,但好处并不大。一般来说,静态库是更好的选择。

如果版本函数要作为更广泛共享库的API的一部分公开,则可能需要考虑第22.5节“符号可见性”和第22.6节“静态和共享库混合”的其他问题。在这种情况下,直接将foobar_version.cpp文件添加到该共享库中可能更合适,而不是为其创建单独的静态库。

21.3. 源代码控制提交

项目希望记录与其源代码控制系统相关的细节并不少见。这可能包括构建时源代码的修订版或提交哈希、当前分支的名称或最近的标签等。上述通过专用的 .cpp 文件提供版本细节的方法非常适合添加更多函数以返回这些细节。例如,可以相对容易地提供当前的 git 哈希:

foobar_version.cpp.in

std::string getFooBarGitHash()
{
 
 return "@FooBar_GIT_HASH@";
}
// Other functions as before...

CMakeLists.txt

cmake_minimum_required(VERSION 3.0)
project(FooBar VERSION 2.4.7)
# The find_package() command is covered later in the Finding Things chapter.
# Here, it provides the GIT_EXECUTABLE variable after searching for the
# git binary in some standard/well-known locations for the current platform.
find_package(Git REQUIRED)
execute_process(
 COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
 RESULT_VARIABLE result
 OUTPUT_VARIABLE FooBar_GIT_HASH
 OUTPUT_STRIP_TRAILING_WHITESPACE
)
if(result)
 message(FATAL_ERROR "Failed to get git hash: ${result}")
endif()
configure_file(foobar_version.cpp.in foobar_version.cpp @ONLY)
# Targets, etc....

一个稍微有趣的例子是测量自某个特定文件改变以来发生了多少次提交。考虑将项目版本嵌入到一个单独的文件中,而不是放在 CMakeLists.txt 文件中,这个单独的文件中只有项目版本号。然后可以合理地假设当版本号发生变化时,该文件也会变化。因此,测量自该文件在当前分支上发生变化以来的提交数量通常是衡量自上次版本更新以来提交数量的良好方法。

以下示例将项目版本移至一个名为 projectVersionDetails.cmake 的单独文件,并通过生成的 foobar_version.cpp 文件中的新函数提供提交数量。它演示了一种适用于任何项目的模式,其中版本是通过顶级 project() 调用设置的,但以一种不会干扰父项目的方式,如果它被纳入更大的项目层次结构中(这是在第30章“FetchContent”中讨论的一个主题)。

foobar_version.cpp.in

unsigned getFooBarCommitsSinceVersionChange()
{
 return @FooBar_COMMITS_SINCE_VERSION_CHANGE@;
}
// Other functions as before...

projectVersionDetails.cmake

# This file should contain nothing but the following line
# setting the project version. The variable name must not
# clash with the FooBar_VERSION* variables automatically
# defined by the project() command.
set(FooBar_VER 2.4.7)
CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
include(projectVersionDetails.cmake)
project(FooBar VERSION ${FooBar_VER})
find_package(Git REQUIRED)
execute_process(
 COMMAND ${GIT_EXECUTABLE} rev-list -1 HEAD projectVersionDetails.cmake
 RESULT_VARIABLE result
 OUTPUT_VARIABLE lastChangeHash
 OUTPUT_STRIP_TRAILING_WHITESPACE
)
if(result)
 message(FATAL_ERROR "Failed to get hash of last change: ${result}")
endif()
execute_process(
 COMMAND ${GIT_EXECUTABLE} rev-list ${lastChangeHash}..HEAD
 RESULT_VARIABLE result
 OUTPUT_VARIABLE hashList
 OUTPUT_STRIP_TRAILING_WHITESPACE
)
if(result)
 message(FATAL_ERROR "Failed to get list of git hashes: ${result}")
endif()
string(REGEX REPLACE "[\n\r]+" ";" hashList "${hashList}")
list(LENGTH hashList FooBar_COMMITS_SINCE_VERSION_CHANGE)
configure_file(foobar_version.cpp.in foobar_version.cpp @ONLY)
# Targets, etc....

以上方法首先找出了版本细节文件最后一次更改的 git 哈希,然后使用 git --rev-list 获取自该提交以来整个存储库的提交哈希列表。提交最初以每行一个哈希的字符串形式找到,然后通过用列表分隔符(;)替换换行字符将其转换为 CMake 列表。然后,list() 命令简单地计算列表中的项目数以给出提交数量。一个更简单的方法是使用 git --rev-list --count 直接获取数量,但是较旧版本的 git 不支持 --count 选项,因此如果需要支持较旧的 git 版本,则以上方法更可取。

还有其他可能的变体。一些项目使用 git describe 提供各种细节,包括分支名称、最近的标签等,但请注意,标签和分支细节可能会更改而不更改提交。如果分支或标签被移动或重命名,构建可能不可重复。如果版本细节仅依赖于文件提交哈希,则不会出现这种弱点。这还为项目在构建确认提交没有错误后自由创建、重命名或删除标签提供了自由(考虑在连续集成构建、测试等确认没有问题后将发布标签应用于提交)。

像 Subversion 这样的源代码控制系统提出了其他挑战。一方面,Subversion 为整个存储库维护全局修订号,因此无需先获取提交哈希然后计算它们的数量。但是,Subversion 也有一个复杂性,即它允许混合不同文件的不同修订版。因此,像上面为 git 概述的方法可能会被开发人员检出不同文件修订版但保持项目版本文件不变的做法所打败。这不是一个人们期望在自动化的持续集成系统中看到的情况,但对于在自己的机器上本地工作的开发人员来说,根据他们的工作方式,这可能更有可能发生。

像上面的技术那样的考虑的另一个方面是是什么导致生成的版本 .cpp 文件被更新。CMake 确保如果项目版本文件发生更改,则重新运行配置步骤,因为它是通过 include() 命令引入到主 CMakeLists.txt 文件中的。但是,如果对其他文件进行了提交,则 CMake 不会意识到。可能可以实现将钩子嵌入版本控制系统(例如 git 的后提交钩子)以强制 CMake 重新运行,但这更可能会使开发人员感到恼火而不是帮助他们。最终,通常会在方便性和鲁棒性之间进行妥协。也就是说,源代码控制细节的准确性可能只对发布非常重要,并且很容易确保发布过程明确调用 CMake。

21.4. 推荐做法

项目不需要遵循任何特定的版本控制系统,但是遵循主.次.补丁.微调的格式,会使得在 CMake 中某些功能变得更加方便,并且新开发者更容易理解项目所使用的版本控制方式。正如将在后面的章节中看到的(特别是第28章,“打包”),版本格式在进行打包发布时更加重要,但由于许多项目在运行时报告自己的版本号,因此版本格式也会影响构建过程。

版本格式中的每个数字的含义取决于项目本身,但是有一些常见的惯例是最终用户经常期望的。例如,主要值的变化通常意味着一个重要的发布,通常涉及不向后兼容的更改或代表项目方向的变化。如果次要值发生变化,用户往往会将其视为一个增量发布,很可能会添加新功能而不会破坏现有行为。当只有补丁值发生变化时,用户可能不会将其视为特别重要的变化,并且期望它相对较小,例如修复一些错误而不引入新功能。微调值通常被省略,并且没有一个普遍的解释,仅仅被认为比补丁更不重要。请注意,这些只是一般观察结果,项目可以为版本号赋予完全不同的含义。为了最简化,一个项目可能只使用一个单独的数字,不加其他内容,实际上将每个发布都指定为一个新的主要版本。虽然这种做法很容易实现,但也会为最终用户提供更少的指导,并且需要良好质量的发布说明来管理每个版本之间的用户期望。

项目()命令的VERSION关键字是CMake提供额外便利性的一个例子,当使用主.次.补丁.微调格式时。项目提供一个单独的版本字符串,项目()命令会自动定义一组变量,使得版本号的各个部分可用。某些CMake模块也可能使用这些变量作为某些元数据的默认值,因此通常建议使用项目()命令的VERSION关键字设置项目版本。此关键字是在CMake 3.0中添加的,但如果要支持较旧的CMake版本,则仍然需要考虑此功能。项目不应该定义与自动定义的变量名称冲突的变量,否则稍后的CMake版本将会发出警告。为了避免这样的警告,避免显式设置名称形式为xxx_VERSION或xxx_VERSION_yyy的变量。

在定义版本号时,考虑将其放在自己专用的文件中,然后通过include()命令将其引入。这允许项目利用版本号与项目源代码控制系统中的文件更改相一致的变化。为了最小化版本更改时的不必要重新编译,生成一个包含返回版本详细信息的 .c 或 .cpp 文件,而不是将这些详细信息嵌入到生成的头文件中或作为编译器定义传递给命令行。还要确保给这些函数命名时包含一些特定于项目的内容,或者将它们放在一个项目特定的命名空间中。这样可以使得相同的模式在许多项目中得以复制,而这些项目后来可能会被合并到单个构建中而不会引起名称冲突。

在项目生命周期的早期建立版本控制策略和实现模式。这有助于开发人员清晰地了解版本详细信息何时以及如何更新,并且在第一次交付的压力之前,就开始考虑发布过程。这还允许在版本号变化和构建周转时间可能变得更重要的发布之前,将效率低下的方法尽早淘汰。