Compile git version inside go binary

Abstract

在我们编写的程序中总是希望可以直接查阅程序的版本,通过--version参数就会输出如下版本信息。

BuildTime: 2018-10-17 17:31:36 +0800 CST (762h17m59.837121151s ago)
BuildHost: gaorong-TM1604
Branch: master
CommitID: acd4a73a5424d3b328c527afea0983d797499ae3
Tags: v0.1.6
Version: v0.1.6
RepoStatus: dirty

我们可以直接将版本信息写在源码里面,但是每次都需要修改源码,比较繁琐; golang 提供了-ldflags flag在编译时动态注入版本信息,生成相关代码,这种方式使用很广泛,本文将会对其进行简单介绍; 最后还会介绍一种特殊的方式: 通过go generate 命令自动生成版本信息代码。

Backgroud

最简单的输出版本信息的方式如下:

package main

import (
"flag"
"fmt"
) var (
version = flag.Bool("version", false, "print version and exit.")
) const (
versionStr = "v0.0.1"
) func main() {
flag.Parse()
if *version {
fmt.Println("version: %s", versionStr)
} // do something...
}

虽然可以打印出程序的版本信息, 但是每次都需要重新修改versionStr变量的值, 比较繁琐。

动态编译版本信息

动态编译版本信息是利用 go build 的 -ldflags 参数在编译时候传入版本信息, 在主程序中定义并使用变量:

package main

import "fmt"

var Version string
var Buildtime string func main() {
fmt.Printf("Version: %s\n", Version)
fmt.Printf("Buildtime: %s\n", Buildtime)
}

在Makefile对上述变量赋值是中并传递进去的,如下:

VERSION := $(shell git rev-parse --short HEAD)
BUILDTIME := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ') GOLDFLAGS += -X main.Version=$(VERSION)
GOLDFLAGS += -X main.Buildtime=$(BUILDTIME)
GOFLAGS = -ldflags "$(GOLDFLAGS)" build:
go build -o mybinary $(GOFLAGS) .

首先获取git的版本信息, 然后在执行go build时, 通过ldflag 参数赋值给main package中的Version, BuildTime变量。

这种方式是使用最为广泛的, docker, kubernetes都是通过这种方式来生成版本信息。

利用go generate生成相关代码

上面动态编译版本信息是通过编译时赋值给变量,其实我们也可以换一种思路:在方法一中我们发现每次版本升级都需要修改代码,替换对应版本信息变量的值,如果我们能够自动化地生成版本信息相关代码,那也可以实现动态编译版本信息。

首先我们新建一个package: buildinfo, 包含buildinfo.go, generat_build_info.go 两个文件。

buildinfo.go 创建buildInfo结构体,包含所有的版本信息及其对应的method:

package buildinfo

//go:generate go run generate-build-info.go

import (
"fmt"
"strings"
"time"
) type buildInfo struct {
Time time.Time
Host string
Branch string
CommitID string
Tags []string
Version string
IsRepoClean bool
} func (bi buildInfo) Show() {
fmt.Printf("BuildTime: %s (%s ago)\n", bi.Time, time.Since(bi.Time))
fmt.Printf("BuildHost: %s\n", bi.Host)
fmt.Printf("Branch: %s\n", bi.Branch)
fmt.Printf("CommitID: %s\n", bi.CommitID)
fmt.Printf("Tags: %s\n", strings.Join(bi.Tags, ", "))
fmt.Printf("Version: %s\n", bi.Version)
repoStatus := "dirty"
if bi.IsRepoClean {
repoStatus = "clean"
}
fmt.Printf("RepoStatus: %s\n", repoStatus)
} // New returns the build time meta variables
func New() buildInfo {
return buildInfo{
Time: buildTime,
Host: buildHost,
Branch: branch,
CommitID: commitID,
Tags: gitTags,
Version: version,
IsRepoClean: isRepoClean,
}
}

可以看到New function会初始化整个结构体, 使用到的变量来自generated_build_info.go 文件。 generated_build_info.go 文件通过名字就可以知道他是一个自动生成的文件,那他是如何自动生成的呐? 仔细查看原来上述代码中包含了一行注释//go:generate go run generate-build-info.go, 就是这一行代码生成的内容。

在golang 中, 提供了go generate subcommand 来自动生成代码, 具体的使用参见官方解释, 使用时在文件前面添加注释//go:generate command , 然后执行 go generate 就会自动搜索找到拥有该注释的文件,并调用指定的command来生成代码, 此处执行的就是go run generate-build-info.gogenerate-build-info.go的内容如下:

// +build ignore

package main

import (
"bytes"
"fmt"
"log"
"os"
"os/exec"
"path"
"strings"
"text/template"
"time"
) var hostname, _ = os.Hostname() const goSourceTemplate = `package buildinfo
// Code generated by go generate; DO NOT EDIT. import "time" var (
buildTime = time.Unix({{ .Timestamp }}, 0)
gitTags = {{ .Tags }}
) const (
buildHost = {{ .Hostname }}
branch = {{ .Branch }}
commitID = {{ .CommitID }}
version = {{ .Version }}
isRepoClean = {{ .IsRepoClean }}
)` func main() {
checkGitEnv()
f, err := os.Create("generated_build_info.go")
if err != nil {
log.Print(err)
os.Exit(1)
}
defer f.Close()
if err := template.Must(template.New("").Funcs(nil).Parse(goSourceTemplate)).Execute(f, struct {
Timestamp int64
Hostname string
Branch string
CommitID string
Tags string
Version string
IsRepoClean bool
}{
Timestamp: time.Now().Unix(),
Hostname: toStrintLiteral(hostname),
Branch: toStrintLiteral(getBranch()),
CommitID: toStrintLiteral(getCommitID()),
Tags: toStringArrayLiteral(getTags()),
Version: toStrintLiteral(getVersion()),
IsRepoClean: isRepoClean(),
}); err != nil {
log.Print(err)
os.Exit(1)
}
} func checkGitEnv() {
gitDir := os.Getenv("GIT_DIR")
if gitDir == "" {
log.Print("GIT_DIR must be set")
os.Exit(1)
}
if gitWorkTree := os.Getenv("GIT_WORK_TREE"); gitWorkTree == "" {
os.Setenv("GIT_WORK_TREE", path.Dir(gitDir))
}
fmt.Printf("Generating build info from GIT_DIR=%s, GIT_WORK_TREE=%s\n", green.T(gitDir), green.T(os.Getenv("GIT_WORK_TREE")))
} func getBranch() string {
return strings.TrimRight(run(`git`, `rev-parse`, `--abbrev-ref`, `HEAD`), "\n")
} func getCommitID() string {
return strings.TrimRight(run(`git`, `rev-parse`, `HEAD`), "\n")
} func getTags() []string {
gitOutput := strings.TrimRight(run(`git`, `tag`, `-l`, `--points-at`, `HEAD`), "\n")
if len(gitOutput) == 0 {
return nil
}
return strings.Split(gitOutput, "\n")
} func isRepoClean() bool {
return run(`git`, `status`, `--short`) == ""
} func getVersion() string {
tags := getTags()
if isRepoClean() && len(tags) == 1 {
return tags[0]
}
return "latest"
} func run(prog string, args ...string) string {
cmd := exec.Command(prog, args...)
bs, err := cmd.Output()
if err != nil {
return ""
}
return string(bs)
} func toStringArrayLiteral(arr []string) string {
var items []string
for _, s := range arr {
items = append(items, toStrintLiteral(s))
}
return fmt.Sprintf("[]string{%s}", strings.Join(items, ", "))
} func toStrintLiteral(s string) string {
return fmt.Sprintf("%q", s)
} // xterm type xterm struct {
f uint8
b uint8
} func (x xterm) T(text string) string {
buf := &bytes.Buffer{}
fmt.Fprintf(buf, "\x1b[%d;%dm", x.b, x.f)
buf.WriteString(text)
buf.WriteString("\x1b[m")
return buf.String()
} var green = xterm{f: 32, b: 1}

可以看到主要就是通过git获取tag作为版本, 并用template库生成内容写入generated_build_info.go文件中,该代码是直接被go run调用必须因此包含main function, 同时buildinfo package只是一个库, 会被其他程序所应用, 一个程序中包含两个main function编译就会报错, 需要在头部添加// +build ignore 注释来声明在编译时不需要包含该文件。最后生成的generated_build_info.go文件内容如下,上面buildinfo.go New function中所使用到的全部变量都是来自于自动生成的这个文件中。

package buildinfo
// Code generated by go generate; DO NOT EDIT. import "time" var (
buildTime = time.Unix(1539768696, 0)
gitTags = []string{"v0.1.6"}
) const (
buildHost = "gaorong-TM1604"
branch = "master"
commitID = "acd4a73a5424d3b328c527afea0983d797499ae3"
version = "latest"
isRepoClean = false
)

上述文件是自动生成的, 无需提交到git中, 所以总共两个文件就构成了buildinfo package,使用的时候在makefile中调用go generate即可。

需要注意的是,在编写程序的时候, buildinfo作为一个独立git package 被主程序所引用,则需要通过GIT_DIR环境变量告诉git获取哪个repo信息, 否则获取到的信息是buildinfo repo而不是主程序repo的信息。 一个完整的命令如下:GIT_DIR=$(PWD)/.git go generate -v $(BUILD_INFO_PKG), 其中BUILD_INFO_PKG 是buildinfo package所在的位置。

总的来说buildinfo package就是通过 go generate 命令结合template 库根据git信息自动生成binary版本信息。

Compile git version inside go binary的更多相关文章

  1. /usr/local/lib/ruby/gems/2.4.0/gems/cocoapods-1.5.3/lib/cocoapods/command.rb:118:in `git_version': Failed to extract git version from `git --version`

    问题及分析 今天做项目的时候,执行pod update报了如下错误信息: /usr/local/lib/ruby/gems/2.4.0/gems/cocoapods-1.5.3/lib/cocoapo ...

  2. CentOS7-安装最新版本GIT(git version 2.18.0)

    Git安装方式有两种一种是yum安装一种是编译安装: 一.yum命令安装,此方法简单,会自动安装依赖的包,而且会从源里安装最新的版本,如果仓库不是最新的话安装的也不是最新Git. sudo yum i ...

  3. Git&Version Control

    Git Git(读音为/gɪt/.)是一个开源的分布式版本控制系统,可以有效.高速地处理从很小到非常大的项目版本管理. [1]  Git 是 Linus Torvalds 为了帮助管理 Linux 内 ...

  4. git version info & svn version info map(七)

    To generate the same version number as SVN, we can generate the same version number as SVN with the ...

  5. Git Version recovery command introduction - git reset

    reset命令有3种方式: git reset –mixed:此为默认方式,不带任何参数的git reset,即时这种方式,它回退到某个版本,只保留源码,回退commit和index信息 git re ...

  6. Error Running Git Empty git --version output:IDEA关联GitHub时出现这个错误

    刚刚学习使用idea中,想要把自己的项目上传到github,遇到这样一个问题,先记录下来,到时候解决了在把方法贴出来. ---------------------------------------- ...

  7. idea Empty git --version output:解决

    在使用idea下的git时候发现报错 但看了一下我的git-bas位置确实没有错啊,也可以启动 后来google了才下发现原来idea的这个地方不用引用的git-bash.exe的路径,而是git.e ...

  8. AS在安装GitHub时出现错误:Empty git --version output:

    AS在安装GitHub时出现错误: 原因:在选择git.exe时选择错误. 解决方法: 选择如下Git下cmd或者bin中的git.exe文件:

  9. git version 2.5.0.windows.1中文乱码问题解决方案

    UI部分 Options->Text Local:zh_CN,Character set:GBK ~/.GitConfig [gui] encoding = utf-8 [tgit] proje ...

随机推荐

  1. nodejs顺序执行shell

    最近工作中需要用到nodejs编写脚本来顺序执行自动化测试用例,编写代码如下: var runCommand = function (command){ child_process.exec(comm ...

  2. js去掉字符串前后空格的五种方法(转)

    出处:http://www.2cto.com/kf/201204/125943.html 第一种:循环检查替换[javascript]//供使用者调用  function trim(s){  retu ...

  3. mongo find

    MongoVUE 对应成语句,结构如下: db.logs.find({ "message" : /消息/ }, { "message" : 1 }).limit ...

  4. 排序:快速排序Quick Sort

    原理,通过一趟扫描将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序 ...

  5. OpenCV的配置

    系统配置:win7 64位系统,编译器 vs2013 一.下载OpenCV安装包(版本2.4.13) https://excellmedia.dl.sourceforge.net/project/op ...

  6. Linux 基础教程 45-read命令

    基本用法     read命令主要用于从标准输入读取内容或从文件中读取内容,并把信息保存到变量中.其常用用法如下所示: read [选项] [文件] 选项 解释 -a array 将内容读取到数值中, ...

  7. IntricCondition和expliciteCondition比较

    IntricCondition 和 expliciteCondition 的区别 与 intrinsicLoc和expliciteLock的区别很相似, expliciteCondition提供了更多 ...

  8. centos mysql忘记密码找回(仅限mysql5.7)

    1.停掉mysql 2.执行#mysqld_safe --user=mysql --skip-grant-tables --skip-networking & 3.#mysql 4.updat ...

  9. akka 练手 原来第一次是原封不动的返回传出去的参数

    今天,有介绍akka的文章,就下了个源码的demo练手! 在TimeServer 这个实例中主要就2个文件 server端 static void Main(string[] args) { usin ...

  10. struts2+ckeditor配置图片上传

    又是一个漫漫长夜. 公司的编辑器坏了,用的是百度编辑器,上传图片的网址被框架给拦截了,我们本地怎么测试都没问题,放到服务器就这样了.和老李找了半天,疯了,没原因的. 笔者以前用过jsp+ckedito ...