在服务端开发中,手动维护版本信息往往既费力又不准确,因为迭代很快,很多情况下都是只更新了代码,而忘记了更新版本,很难保证一致性。
我们可以使用版本控制系统提供的信息——比如: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 --tagsv1.0.1
   复制代码
 获取当前分支的 commit id
如果你不使用 Tag 进行上线,那可以用 Commit ID 作为版本号:
 > git rev-parse --short HEAD23cc251
   复制代码
 PS. 这里只演示如何在 git 中获取版本信息,如果你使用其他版本控制系统,请查阅对应的文档
在得到版本信息之后,我们看下如何在不同的语言中进行注入:
C/C++
在 C/C++ 中,需要结合 CMake 把版本信息编译进 binary 内,共分为 2 个步骤:
- 使用 EXECUTE_PROCESS()获取 git 版本 
- 使用 CONFIGURE_FILE()生成头文件 
使用 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。 
结果输出:
PS. EXECUTE_PROCESS 功能非常强大,这里只用到了几个基本的参数,详细介绍请参照CMake文档。
在实践中还有一个问题需要解决:有时候,我们不一定在源码目录下进行构建,而是在另一个目录中,比如代码在 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)
   复制代码
 PS. GIT_DIR 对大部分 git 命令都生效~
加上这行后,再次运行,结果正常:
使用 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 的 PROJECT_BINARY_DIR 目录)就生成了 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、运行时版本、核心代码的时间统计等等。它们不是业务数据,而是关于代码或服务本身的数据,它让代码自己“说话”,把当前最真实的情况反映给开发者,也为后续一些自动化的操作提供了可能。
希望本文能对你有所帮助,有什么想法和意见可以留言告诉我~
封面图,Photo by Yancy Min on Unsplash
关注个人公众号 WhatHowWhy 获得及时更新
评论