1. 背景

当一个软件产品,无论独立进程还是运行库,发布到不同现场、不同机器上运行一年半载后,往往或调试bug、或改善性能、或加入新功能,需要基于当初发布的版本来增量开发,如果找不到现场软件对应代码仓库中哪一次提交,这项工作无法精准开展,只能基于某个近似版本开发,此时涉及到几项工作:

  1. 当初发布版本和近似版本存在新功能、性能、或解决过一些bug的差异,现在得重新填坑。
  2. 如果直接使用最新版本的软件产品,往往会因为由于新版依赖上下游不同的库、或不同的通信协议、或进程间不同的交互协议,导致工作量更大。

最后只能选择方案1发布到现场测试,你自己都不知道会发生什么,好比瞎子上战场,面对枪林弹雨。

这样的软件自然无法维护,成为烂尾。

2. 对策

下面提供一种以微软VS开发环境为例、集成Git代码仓库的解决方法。其中2. Python3脚本方法运用环境更广泛。

大概是利用VS执行编译之前的事件,去调用某个BAT文件,该文件获取当前仓库的最后git提交号,写入到头文件,后续实际软件代码包含该头文件,再提供一个API,让外部读取该git提交号。

2.1. BAT方法

  1. 编辑VS工程属性,在其预先生成事件的命令行加入$(SolutionDir)\make_git_version.bat
  2. 此时再编译工程会调用该bat,并产生git_version.h文件。

make_git_version.bat代码如下:

@echo on

set cmd="git describe --abbrev=7 --dirty --always --tags"

FOR /F "tokens=*" %%i IN (' %cmd% ') DO SET git_commit_id=%%i

set tmp_h=git_version.h.tmp
set git_version_h=git_version.h

echo #define GIT_VERSION #GIT_VERSION# >> %tmp_h%

powershell -Command "(Get-Content %tmp_h%) | ForEach-Object { $_ -replace '#GIT_VERSION#', '\"%git_commit_id%\"' } | Set-Content %git_version_h%"

del %tmp_h%

git_version.h内容示例:

#define GIT_VERSION "20a688e" 

2.2. Python3脚本方法

支持C/C++/CSharp多种语言。

  1. 编辑VS工程属性,在其预先生成事件的命令行加入python git_version.py
  2. 此时再编译工程会调用该脚本,并产生git_version.h文件。
  3. 产生CSharp语言文件时使用python git_version.py -o git_version.cs,会产生git_version.cs文件,内容大概如下:
    class GitVersion
    {
     public static string GIT_VERSION = "87a96be";
    }
    

git_version.py代码示例:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os, math, sys, platform, time, signal, fnmatch
import getopt
import hashlib
import threading, subprocess, re
import unittest

from optparse import OptionParser
from datetime import datetime
from functools import reduce

_g_version = "v1.0.0"

def get_timestamp():
    return time.strftime("%Y%m%d%H%M%S", time.localtime(time.time()))

def execute_command(cmd):
    exit_code = -1
    output = ""
    try:
        proc = subprocess.Popen(cmd, shell=True
                                , stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        while True:
            line = proc.stdout.readline()            
            if not line: 
                break
            line = line.decode('utf-8')
            line = line.replace("'",'')
            line = line.strip()
            output += line
        
        exit_code = proc.wait()
    except:
        print("cmd {} occur error.".format(cmd))
    finally:
        return exit_code, output

def main(options):
    exit_code = 0

    cmd = "git describe --abbrev=7 --dirty --always --tags"
    exit_code, output = execute_command(cmd)
    print(output)
    fd = open(options.output, 'w')

    if options.output.endswith(".h") \
        or options.output.endswith(".c") \
        or options.output.endswith(".cpp"):
        data = "#define GIT_VERSION \"{}\"".format(output)
    elif options.output.endswith(".cs"):
        data = "class GitVersion " %(output)
    fd.write(data)
    fd.close()

    sys.exit(exit_code)

if __name__ == '__main__':
    appname = os.path.basename(sys.argv[0]).split(".")[0]
    usage = "usage: %prog [options] arg"
    parser = OptionParser(usage)
    parser.add_option("-o", "--output", dest="output"
                    , default="git_version.h", help="output")
    parser.add_option("-v", "--version", action="store_true", dest="version"
                    , help="print version")
    (options, args) = parser.parse_args()
    if options.version is not None:
        print("{}".format(_g_version))
        sys.exit(0)

    main(options)

2.3. 编写获取版本号代码如version.h,其会包含git_version.h,并读取git提交号

#ifndef __VERSION_H__
#define __VERSION_H__

#include "git_version.h"

#ifdef __cplusplus
extern "C" {
#endif

#define VERSION "V2.1.0"
#define STARE "RC"

static const char *get_version(void)
{
    static char version[128];

    if (version[0] == 0x00) {
        static char *mon_tables[] = {
            "Jan", "Feb", "Mar", "Apr", "May", "Jun", 
            "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
        };
        char date[16] = {0};
        char year[16] = {0};
        char month[16] = {0};
        char day[16] = {0};
        int i;
#ifdef _WIN64
        char *platform = "64Bits";
#else
        char *platform = "32Bits";
#endif
    
#ifdef _DEBUG
        char *configure = "Debug";
#else
        char *configure = "Release";
#endif

        strcpy_s(date, sizeof(date), __DATE__);
        sprintf_s(year, sizeof(year), 
                    "%c%c%c%c/", date[7], date[8], date[9], date[10]);

        memmove(month, date, 3);
        for (i = 0; i < sizeof(mon_tables) / sizeof(char *); i++) {
            if (strcmp(mon_tables[i], month) == 0)
                break;
        }
        sprintf_s(month, sizeof(month), "%02d/", i+1);

        memmove(day, &date[4], 2);
        day[0] = day[0] == ' ' ? '0' : day[0];

        sprintf_s(version, sizeof(version), "%s, %s, git %s, %s/%s, build time: %s%s%s %s"
                    , VERSION, STARE, GIT_VERSION, configure
                    , platform, year, month, day, __TIME__);
    }
    return version;
}

#ifdef __cplusplus
}
#endif
#endif

2.4. 编写读出版本号代码

# include "version.h"

int main(int argc, char **argv)
{
    printf("My software version is %s\n", get_version());    
    return 0;
}

输出

My software version is  Version:V2.1.0, RC, git faf8378 Debug/32Bits, build time: 2018/09/17 18:22:42

如果是库形式软件产品,可提供一个API供使用者调用,其实现返回get_version()的返回值即可,需注意的是该返回值对应内存区域属于Data段,而不是堆,故不允许调用free释放。

2.5. 发布软件

每次较大修改后可修改上面versioh.hVERSION,这是大版本号,git提交号只作为回溯git代码仓库提交。

2.6. 备注

文中以Visual Studio和C/C++语言为例,如果SVN或C#,可采用类似BAT的方式,采用Python语言时也类似,只要用好"git describe --abbrev=7 --dirty --always --tags"命令即可。