让代码说话:如何把版本信息注入到代码中
在服务端开发中,手动维护版本信息往往既费力又不准确,因为迭代很快,很多情况下都是只更新了代码,而忘记了更新版本,很难保证一致性。
我们可以使用版本控制系统提供的信息——比如:Tag name、Commit id、Revision id等——作为软件版本,在部署阶段自动注入到代码里。这样既能保证信息真实可回溯,也避免了手动操作的麻烦。
本文介绍了如何在C/C++/Go/Rust/Python中整合上述信息的方法:
获取版本信息
如果你是通过 Tag 上线,而且代码运行在 Docker 容器里,那我们可以把 Tag 名定义为 Dockerfile 的 ARG 参数,在构建镜像时通过 -build-arg 传入:
# Dockerfile
ARG CODEGITTAG=default_version
# using ${CODEGITTAG}
> docker build --build-arg CODEGITTAG=v.2021012801 ...
如果你的代码没有容器化,可以用下面的命令从git中读取。
获取Tag名
> git checkout v1.0.1
....
> git describe --tags
v1.0.1
获取当前分支的commit id
如果你不使用Tag进行上线,那可以用Commit ID作为版本号:
> git rev-parse --short HEAD
23cc251
PS. 这里只演示如何在git中获取版本信息,如果你使用其他版本控制系统,请查阅对应的文档
在得到版本信息之后,我们看下如何在不同的语言中进行注入:
C/C++
在 C/C++ 中,需要结合CMake把版本信息编译进 binary 内,共分为2个步骤:
使用 EXECUTE_PROCESS() 获取git版本
在CMakeLists.txt中增加以下代码:
EXECUTE_PROCESS(
COMMAND git rev-parse --short HEAD
OUTPUT_VARIABLE MAGE_VERSION_GIT_HEAD_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_VARIABLE GET_GIT_VERSION_FAILED
)
IF(GET_GIT_VERSION_FAILED)
MESSAGE(FATAL_ERROR ${GET_GIT_VERSION_FAILED})
ELSE(GET_GIT_VERSION_FAILED)
MESSAGE("-- Current Git Commit ID: ${MAGE_VERSION_GIT_HEAD_VERSION}")
ENDIF(GET_GIT_VERSION_FAILED)
EXECUTE_PROCESS() 用到的参数:
COMMAND ...,这里是获取 Commit ID,也可以获取 Tag Name。
OUTPUT_VARIABLE ..., 保存命令输出,即我们需要的版本信息。
OUTPUT_STRIP_TRAILING_WHITESPACE, 因为输出时会带上一个换行,会导致生成的代码格式不正确,所以我们用这个参数把它给抹掉。
ERROR_VARIABLE ..., 保存执行失败的错误输出。
最后5行是检查错误,并把版本信息打印出来以便于Debug。
结果输出:

在实践中还有一个问题需要解决:有时候,我们不一定在源码目录下进行构建,而是在另一个目录中,比如代码在 src/your-git-src-directory,而构建在 build/your-build-directory。可是build目录并不是一个 git 文件夹,如果我们运行cmake,会得到如下错误提示:

这时,我们只要设置环境变量GIT_DIR就可以解决这个问题了,如下第一行:
SET(ENV{GIT_DIR} ${PROJECT_SOURCE_DIR}/.git) # <-----
EXECUTE_PROCESS(
COMMAND git rev-parse --short HEAD
OUTPUT_VARIABLE MAGE_VERSION_GIT_HEAD_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_VARIABLE GET_GIT_VERSION_FAILED
)
IF(GET_GIT_VERSION_FAILED)
MESSAGE(FATAL_ERROR ${GET_GIT_VERSION_FAILED})
ELSE(GET_GIT_VERSION_FAILED)
MESSAGE("-- Current Git Commit ID: ${MAGE_VERSION_GIT_HEAD_VERSION}")
ENDIF(GET_GIT_VERSION_FAILED)
加上这行后,再次运行,结果正常:

使用 CONFIGURE_FILE生成版本头文件
在源码目录增加一个 config.h.cmake 的文件:
#define MAGE_VERSION_MAJOR @MAGE_VERSION_MAJOR@
#define MAGE_VERSION_MINOR @MAGE_VERSION_MINOR@
#define MAGE_VERSION_PATCH @MAGE_VERSION_PATCH@
#define MAGE_VERSION_GIT_HEAD_VERSION "@MAGE_VERSION_GIT_HEAD_VERSION@"
其中major、minor和patch是传统的软件版本规范,如果你的项目里不需要的话,可以删除掉,如果保留就需要在CMake中赋值。除了这3个之外,就是最重要的 "@MAGE_VERSION_GIT_HEAD_VERSION@",即我们从git里获取的版本信息,它要和EXECUTE_PROCESS()指令的OUTPUT_VARIABLE的变量名保持一致,注意我们用双引号把它括起来当作一个字符串使用。
增加以下代码生成头文件:
configure_file("src/config.h.cmake" "config.h")
运行 cmake,在当前目录下(准确的说是 cmake 的 目录)就生成了config.h 文件:
#define MAGE_VERSION_MAJOR 2
#define MAGE_VERSION_MINOR 0
#define MAGE_VERSION_PATCH 0
#define MAGE_VERSION_GIT_HEAD_VERSION "305fdd9"
我们可以在代码里包含这个文件,并进行相关的操作。
Go
在 Go 语言中注入版本信息比较简单,使用 go build 的 ldflags 参数就可以直接给代码里的Variable进行赋值!请看下面的测试项目:

core.go 文件中定义了一个全局 Variable(CodeVersion)和3个helper function:
// core.go
package core
import (
"fmt"
"runtime"
)
var (
CodeVersion = ""
)
func RuntimeVersion() string {
return fmt.Sprintf("%s %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH)
}
func CodeBaseVersion() string {
return CodeVersion
}
func Version() string {
return fmt.Sprintf("%s; %s", CodeBaseVersion(), RuntimeVersion())
}
接着是main.go:
// main.go
package main
import (
"flag"
"fmt"
"github.com/zhujun1980/goversion/core"
)
func main() {
var ver = flag.Bool("version", false, "show version")
flag.Parse()
if *ver {
fmt.Printf("%s\n", core.Version())
return
}
}
编译 goversion 并把版本信息写进去:
> go build -ldflags="-X 'github.com/zhujun1980/goversion/core.CodeVersion=v.2021012901'"
这里假设你已经获得到了代码的版本信息,如果没有,可以从命令行直接调用git命令:
> go build -ldflags="-X 'github.com/zhujun1980/goversion/core.CodeVersion=$(git describe --tags)'"
ldflags 的 -X 参数根据 package 路径直接对变量进行赋值,注意是全路径。执行结果:

我们还可以把构建时间写进去:
// core.go
package core
import (
"fmt"
"runtime"
)
var (
CodeVersion = ""
BuildTime = ""
)
func RuntimeVersion() string {
return fmt.Sprintf("%s %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH)
}
func CodeBaseVersion() string {
return CodeVersion
}
func Version() string {
return fmt.Sprintf("%s; %s; %s", CodeBaseVersion(), RuntimeVersion(), BuildTime)
}
在编译的时候如法炮制——把时间传进去:
go build -ldflags="-X 'github.com/zhujun1980/goversion/core.CodeVersion=v.2021012902' -X 'github.com/zhujun1980/goversion/core.BuildTime=$(date -R)'"
运行程序:

用这个方法我们可以把任何信息都写进去:服务器IP,用户ID,当地天气,生辰八字、当天黄历 ;-)
Rust
在 Rust 里面,需要使用 Cargo 的 build script 来生成文件,Cargo 是 Rust 自带的构建系统,build script 一般用来编译非 Rust 第三方代码(比如C++),它在编译正式源码之前被调用,也可以用来生成代码。在Cargo的文档中提供了一个例子,我们把它修改一下来实现我们的需求:
// build.rs
use rustc_version::version;
use std::env;
use std::format;
use std::fs;
use std::path::Path;
fn main() {
let out_dir = env::var_os("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("version.rs");
fs::write(
&dest_path,
format!(
"pub fn version() -> &'static str {{
\"{}; {} {}/{}\"
}}
",
match env::var("CODE_BASE_VERSION") {
Ok(val) => val,
Err(_) => "".to_string(),
},
version().unwrap(),
match env::var("CARGO_CFG_TARGET_OS") {
Ok(val) => val,
Err(_) => "".to_string(),
},
match env::var("CARGO_CFG_TARGET_ARCH") {
Ok(val) => val,
Err(_) => "".to_string(),
}
),
)
.unwrap();
println!("cargo:rerun-if-changed=build.rs");
}
# main.rs
include!(concat!(env!("OUT_DIR"), "/version.rs"));
fn main() {
println!("{}", version());
}
main.rs 很简单,只引入了 version.rs 文件并打印调用结果;主要功能在 build.rs 文件:生成 version.rs 文件,返回我们的代码版本,Rust 编译器的版本,操作系统、CPU架构等信息。
注意为了能获取 Rust 编译器的版本,需要在Cargo.toml增加构建依赖:
# Cargo.toml
[build-dependencies]
rustc_version = "0.3.3"
编译这个程序,同时设置 CODE_BASE_VERSION 环境变量为代码版本:
> CODE_BASE_VERSION=v.2021013001 cargo build
成功之后会在 OUT_DIR 目录生成一个 version.rs 文件:
// version.rs
pub fn version() -> &'static str {
"v.2021013001; 1.48.0 macos/x86_64"
}
运行程序:

关于 Cargo build script 更详细的介绍,请参见它的文档。
Python等解释型语言
在Python这样的解释型语言中,因为没有编译的过程,所以要在上线部署时完成版本信息注入,比如在构建镜像时。
我们建立一个如下的代码模版:
# version.py
import platform
def codebase_version():
return "CODE_BASE_VERSION"
def py_version():
return "{}{} {}/{}".format(platform.python_implementation(),
platform.python_version(),
platform.system(),
platform.machine())
def version():
return "{}; {}".format(codebase_version(), py_version())
if __name__ == "__main__":
print(version())
函数 codebase_version() 中的 CODE_BASE_VERSION 是需要被替换的版本信息,在构建时,使用 sed 命令进行替换:
> CODE_BASE_VERSION=v.2021013101
> sed -i -e 's|CODE_BASE_VERSION|'$CODE_BASE_VERSION'|g' version.py
CODE_BASE_VERSION 会被替换为 v.2021013101,注意:CODE_BASE_VERSION 中不能包含 "|",否则会和 sed 的正则分隔符冲突。
运行程序:

显示版本信息
除了在命令行中显示版本信息,我们还可以在服务接口中返回它。比如在返回内容中增加一个version字段等:

总结
本文介绍了如何在不同语言中,把版本信息注入到代码的自动化方法,从而避免了手动维护版本的繁琐,并提高了准确性。版本是代码的一个“属性”,推而广之,我们还可以把更多类似的“属性”在服务中体现出来,比如编程语言、服务器IP、运行时版本、核心代码的时间统计等等。它们不是业务数据,而是关于代码或服务本身的数据,它让代码自己“说话”,把当前最真实的情况反映给开发者,也为后续一些自动化的操作提供了可能。
希望本文能对你有所帮助,有什么想法和意见可以留言告诉我~
关注个人公众号 WhatHowWhy 获得及时更新
