写点什么

让代码说话:如何把版本信息注入到代码中

用户头像
zhujun
关注
发布于: 2021 年 02 月 20 日
让代码说话:如何把版本信息注入到代码中

在服务端开发中,手动维护版本信息往往既费力又不准确,因为迭代很快,很多情况下都是只更新了代码,而忘记了更新版本,很难保证一致性。

我们可以使用版本控制系统提供的信息——比如: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 个步骤:

  1. 使用 EXECUTE_PROCESS()获取 git 版本

  2. 使用 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 获得及时更新


发布于: 2021 年 02 月 20 日阅读数: 23
用户头像

zhujun

关注

心怀理想,接受现实 2021.02.20 加入

中老年程序员,老师傅

评论

发布
暂无评论
让代码说话:如何把版本信息注入到代码中