在服务端开发中,手动维护版本信息往往既费力又不准确,因为迭代很快,很多情况下都是只更新了代码,而忘记了更新版本,很难保证一致性。
我们可以使用版本控制系统提供的信息——比如: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 版本
使用 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 获得及时更新
评论