zoukankan      html  css  js  c++  java
  • Compile git version inside go binary

    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
    ", Version)
    	fmt.Printf("Buildtime: %s
    ", 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)
    ", bi.Time, time.Since(bi.Time))
    	fmt.Printf("BuildHost: %s
    ", bi.Host)
    	fmt.Printf("Branch: %s
    ", bi.Branch)
    	fmt.Printf("CommitID: %s
    ", bi.CommitID)
    	fmt.Printf("Tags: %s
    ", strings.Join(bi.Tags, ", "))
    	fmt.Printf("Version: %s
    ", bi.Version)
    	repoStatus := "dirty"
    	if bi.IsRepoClean {
    		repoStatus = "clean"
    	}
    	fmt.Printf("RepoStatus: %s
    ", 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
    ", green.T(gitDir), green.T(os.Getenv("GIT_WORK_TREE")))
    }
    
    func getBranch() string {
    	return strings.TrimRight(run(`git`, `rev-parse`, `--abbrev-ref`, `HEAD`), "
    ")
    }
    
    func getCommitID() string {
    	return strings.TrimRight(run(`git`, `rev-parse`, `HEAD`), "
    ")
    }
    
    func getTags() []string {
    	gitOutput := strings.TrimRight(run(`git`, `tag`, `-l`, `--points-at`, `HEAD`), "
    ")
    	if len(gitOutput) == 0 {
    		return nil
    	}
    	return strings.Split(gitOutput, "
    ")
    }
    
    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版本信息。

  • 相关阅读:
    Redis 设置密码登录
    SELinux 宽容模式(permissive) 强制模式(enforcing) 关闭(disabled) 几种模式之间的转换...
    laravel 博客项目部署到Linux系统后报错 权限都设置为777,仍然报错没有权限
    linux用netstat查看服务及监听端口
    redis使用rediscli查看所有的keys及清空所有的数据
    一起谈.NET技术,Oxite 项目结构分析 狼人:
    一起谈.NET技术,VS 2010 和 .NET 4.0 系列之《在VS 2010中查询和导航代码》篇 狼人:
    一起谈.NET技术,VS 2010 和 .NET 4.0 系列之《添加引用对话框的改进》篇 狼人:
    一起谈.NET技术,VS 2010 和 .NET 4.0 系列之《代码优化的Web开发Profile》篇 狼人:
    一起谈.NET技术,数组排序方法的性能比较(3):LINQ排序实现分析 狼人:
  • 原文地址:https://www.cnblogs.com/gaorong/p/10017048.html
Copyright © 2011-2022 走看看