对于一个开发桌面程序的小团队来说,如果 一没有专门人员负责软件打包发布,二没有CI/CD系统进行自动发布,且需要频繁迭代的话,开发工程师最好保证每次代码提交后,生成的产物自动生成附带不同的版本号。不然在迭代过程中,和测试人员或用户交流会有一定困难。
对于使用qmake或cmake等构建工具的工程,可以直接:
- 在工程文件中通过git获取仓库信息,而后通过宏定义传递到源码;
- 或者构建过程中生成一个临时文件,作为源码一部分使用。
那么对于一个C#的WinForm程序,如何在Visual Studio下每次构建时自动调用git来获取仓库信息呢?
其实也简单,只需要一个.tt文件进行了。接下来,我们使用Visual Studio 2019自带 T4 功能,一步一步实现如下效果:
即:在C#程序 About对话框 以及在程序 属性对话框 中显示版本信息。那么:
如何确定版本号?
其实,版本号定义本身比较随意的,比如大佬 高德纳 使用圆周率 π
和自然底数e
作为他软件作品的版本号。截至目前Tex的版本号是3.141592653。
语义化版本(SemVer)在开源社区用的比较广,文档中各字段定义也很详细。简单说,版本号一般由三部分构成:
而微软的定义是这样的(一直不清楚它的Build number是怎么用的):
| Major.Minor.Build.Revision
|
在本文中,我们使用4个数字来表示版本号,比如 1.0.0.2。
其中,前三位是代码中或配置文件中写死的(有重大改动时可手动升版),最后一位是自动生成的,只需要保证最后一位单调增加就行(当然,不同人在不同时间编译同一个版本,最后一位也要保持一致)。
老的SVN干这个事情很简单,反而Git作为分布式源码控制系统,做这个事情还真不擅长,它只保证每次提交生成一个长长的SHA1值,但是这个值又对用户没什么用。简单起见,我们可以使用git仓库的提交数或者距离上一个tag的提交数作为版本号的最后一位。
尽管代码每次提交生成的长长的SHA1值对客户没有用,但是对偷懒的程序员来说,实在太有用了,依靠它,随时可以找到这个版本对应的源码。所以这个东西我们此次也显示出来了,不过SHA1也不用全显示,一般显示开头的几个数字就够了。如果仓库中有tag,使用describe获取描述信息也是不错选择。
信息放什么地方?
在C#中,版本信息一般都存放在一个名为 AssemblyInfo.cs的文件中。文件内容大致如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("Debao's C# Demo")]
[assembly: AssemblyDescription("All right reserved.")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Demo")]
[assembly: AssemblyCopyright("Copyright © 2023")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: AssemblyInformationalVersion("")]
[assembly: AssemblyVersion("1.0.0.2")]
[assembly: AssemblyFileVersion("1.0.0.2")]
[assembly: AssemblyMetadata("BuildDate", "11/21/2023 12:50:42")]
[assembly: AssemblyMetadata("GitSHA1", "4f3935e")]
|
如果只想在About对话框显示版本信息,用不用 AssemblyInfo.cs 是无所谓的,随便创建一个文件,写入需要的信息,让About对话框调用就可以了。
但使用这个文件可以同时解决版本显示的两个问题:
- 在属性对话框中显示(在程序图标上点击右键)
- 在About 对话框中显示
代码每次变化,都需要这个文件信息发生变化,该怎么办...
如何自动更新AssemblyInfo.cs?
既然需要自动更新,那么这个文件就不适合放进代码仓库。
创建一个 AssemblyInfo.tt 文件,在构建过程中,使用T4 自动将其转成对应的 .cs文件。
准备
首先,删除AsemblyInfo.cs这个文件
其次,保证 git 不管控这个文件,在 .gitignore
中添加:
| Properties/AssemblyInfo.cs
|
主角 T4
T4(Text Template Transformation Toolkit)文本模板转换工具,是一个开源的基于模板的文本生成框架。T4 源文件的后缀通常使用 .tt。
T4 文本模板是可生成文本文件的文字块和控制逻辑的混合。控制逻辑可以用Visual C# 或 Visual Basic的程序代码段书写。生成的文件可以是文本,资源文件,或程序源文件。
.tt文件 本身和 .cs 很像,如下所示,只有最后几行不同。在这几行中,我们调用自定义的函数,用来获取源码仓库信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Debao's Demo")]
[assembly: AssemblyDescription("All right reserved.")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Demo")]
[assembly: AssemblyCopyright("Copyright © 2023")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: AssemblyInformationalVersion("")]
[assembly: AssemblyVersion("1.0.0.<#= GetCommitsCount() #>")]
[assembly: AssemblyFileVersion("1.0.0.<#= GetCommitsCount() #>")]
[assembly: AssemblyMetadata("BuildDate", "<#= DateTime.Now #>")]
[assembly: AssemblyMetadata("GitSHA1", "<#= GetSha1().Substring(0,7) #>")]
|
这两个函数的意图很明显,只需要将其也定义在这个文件中就行了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51 | <#+
public int GetCommitsCount()
{
int count = 0;
using (Process process = new Process())
{
process.StartInfo.FileName = "git";
process.StartInfo.Arguments = "rev-list --count HEAD";
process.StartInfo.UseShellExecute = false;
process.StartInfo.WorkingDirectory = Path.GetDirectoryName(this.Host.TemplateFile);
process.StartInfo.RedirectStandardOutput = true;
try
{
process.Start();
StreamReader reader = process.StandardOutput;
string output = reader.ReadToEnd();
count = int.Parse(output);
}
catch(Exception ex)
{
// fail to run the git command.
}
}
return count;
}
public string GetSha1()
{
string sha1="";
using (Process process = new Process())
{
process.StartInfo.FileName = "git";
process.StartInfo.Arguments = "rev-parse HEAD";
process.StartInfo.UseShellExecute = false;
process.StartInfo.WorkingDirectory = Path.GetDirectoryName(this.Host.TemplateFile);
process.StartInfo.RedirectStandardOutput = true;
try
{
process.Start();
StreamReader reader = process.StandardOutput;
sha1 = reader.ReadToEnd();
}
catch(Exception ex)
{
// fail to run the git command.
}
}
return sha1;
}
#>
|
通过直接调用git获取所需的信息,代码有些简单粗暴,就不解释了。
只不过,这几个函数,使用了.net一些标准库,所以使用之前需要通过 import 引入进来,如下:
| <#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Collections.Generic" #>
|
这三部分合起来,就是完整的所需要的文件...
Assembly.tt 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93 | <#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Collections.Generic" #>
//---------------------------------------------------------------------------
// This file is auto-generated by T4 from AssemblyInfo.tt
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
//---------------------------------------------------------------------------
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Debao's Demo")]
[assembly: AssemblyDescription("All right reserved.")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Demo")]
[assembly: AssemblyCopyright("Copyright © 2023")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: AssemblyInformationalVersion("")]
[assembly: AssemblyVersion("1.0.0.<#= GetCommitsCount() #>")]
[assembly: AssemblyFileVersion("1.0.0.<#= GetCommitsCount() #>")]
[assembly: AssemblyMetadata("BuildDate", "<#= DateTime.Now #>")]
[assembly: AssemblyMetadata("GitSHA1", "<#= GetSha1().Substring(0,7) #>")]
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("14b8063a-65bf-4794-84d0-e6b61ee99ebb")]
<#+
public int GetCommitsCount()
{
int count = 0;
using (Process process = new Process())
{
process.StartInfo.FileName = "git";
process.StartInfo.Arguments = "rev-list --count HEAD";
process.StartInfo.UseShellExecute = false;
process.StartInfo.WorkingDirectory = Path.GetDirectoryName(this.Host.TemplateFile);
process.StartInfo.RedirectStandardOutput = true;
try
{
process.Start();
StreamReader reader = process.StandardOutput;
string output = reader.ReadToEnd();
count = int.Parse(output);
}
catch(Exception ex)
{
// fail to run the git command.
}
}
return count;
}
public string GetSha1()
{
string sha1="";
using (Process process = new Process())
{
process.StartInfo.FileName = "git";
process.StartInfo.Arguments = "rev-parse HEAD";
process.StartInfo.UseShellExecute = false;
process.StartInfo.WorkingDirectory = Path.GetDirectoryName(this.Host.TemplateFile);
process.StartInfo.RedirectStandardOutput = true;
try
{
process.Start();
StreamReader reader = process.StandardOutput;
sha1 = reader.ReadToEnd();
}
catch(Exception ex)
{
// fail to run the git command.
}
}
return sha1;
}
#>
|
确保文件属性中,Custom Tool 选择:TextTemplatingFileGenerator
至此,任务完成。每次重新构建项目时,AssemblyInfo.cs 都会自动生成出来,如果不重新构建项目,也可以在AssemblyInfo.tt上点击右键,选择"Run Custom Tool"。
题外
如果要在没有git的机器上进行构建,或者不想直接调用git。可以搜索到 .git 文件夹,直接解析它。比如,上面用到的GetSha1()函数,也可以像下面这样写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 | public string GetHeadSha1(DirectoryInfo gitDirectory)
{
if (gitDirectory != null)
{
var head = File.ReadAllText(gitDirectory.GetFiles("HEAD").First().FullName);
head = head.Substring(4).Trim();
var headRef = System.IO.Path.Combine(gitDirectory.FullName, head);
return File.ReadAllText(headRef).Trim();
}
return null;
}
public DirectoryInfo SearchForGitDirectory(string path)
{
DirectoryInfo di = new DirectoryInfo(path);
var directories = di.GetDirectories(".git");
var gitDirectory = directories.FirstOrDefault(x => x.Name == ".git");
if (gitDirectory == null)
{
if (di.Parent != null)
{
return SearchForGitDirectory(di.Parent.FullName);
}
return null;
}
return gitDirectory;
}
public string GetSha1()
{
var gitDirectory = SearchForGitDirectory(Path.GetDirectoryName(this.Host.TemplateFile));
var headSha1 = GetHeadSha1(gitDirectory);
return headSha1;
}
|
当然,如果真要在没有安装git的机器上使用git仓库,就真有点自虐了...
参考