🆚

Gitのタグやハッシュの情報をアセンブリに埋め込む(.NET Framework)

2022/01/05に公開

.NET Frameworkのプロジェクトで、アセンブリにGitのタグやハッシュの情報を埋め込む方法を記載します。

動作確認時のバージョンは下記の通りです。

  • Visual Studio 2022
  • .NET Framework 4.7.2

言語はC#を利用して確認しています。

下記に動作確認に使ったプロジェクトを登録しています。

埋め込む情報

どのような状態で生成されたものかわかるように、下記のように設定することにします。

  • AssemblyVersion : Gitのタグを元にファイルバージョンの形式にしたもの。
    • タグがv1.1.2ならば、1.1.2.0となるように。
      v1.0.0-beta1となっていたら、-beta1の部分も除去する。(フォーマットとして許容されないので)
  • AssemblyFileVersion : AssemblyVersion と同じ。
    • タグがv1.1.2ならば、1.1.2となるように。
      AssemblyVersion とは桁数が異なる。
  • AssemblyInformationalVersion : Gitのタグ+コミットハッシュ値も付与。
    • タグがv1.1.2ならば、ハッシュ(8桁)も付与して1.1.2.f15325d3となるように。
      v1.0.0-beta1となっていたら、-beta1の部分も付けたまま1.1.2-beta1.f15325d3といった形で。(AssemblyInformationalVersionAssemblyVersionのような制限がなく自由)

MSBuild Community Tasks

.NET Frameworkのプロジェクトだと、どうやってAssemblyInfo.csに各種情報を動的に埋め込むのかっていうところがポイントになります。

今回はMSBuild Community Tasksというライブラリを利用します。

NuGetではMSBuildTasksという名前になっています。

このライブラリでは、様々な便利なタスクが提供されています。
その中の下記タスクを利用して実現します。

  • GitVersion : Gitのコミットハッシュを取得するタスク
  • GitDescribe : 直近のタグの情報(git describeで取得できる情報)を取得するタスク
  • RegexReplace : 正規表現による置換を行うタスク
  • AssemblyInfo : AssemblyInfo.csなどを生成するタスク

MSBuild Community Tasks のインストール

ソリューションのNuGetパッケージの管理か、パッケージマネージャーコンソールでインストールします。
パッケージ名はMSBuildTasksになります。

nuget

BeforeBuild ターゲットの追加

BeforeBuildターゲットでAssemblyInfo.csを生成し、そこでGitの情報を埋め込むようにします。

プロジェクトファイル(*.csproj)に追加します。

  <Target Name="BeforeBuild">
    <MakeDir Directories="Properties" />
    <GitVersion ShortLength="8">
      <Output TaskParameter="CommitHash" PropertyName="CommitHash" />
    </GitVersion>
    <GitDescribe Command="describe --first-parent" LightWeight="True" Match="v[0-9]*">
      <Output TaskParameter="Tag" PropertyName="VersionTag" />
    </GitDescribe>
    <RegexReplace  Input="$(VersionTag)" Expression="v([0-9\.]+).*" Replacement="$1">
      <Output TaskParameter="Output" PropertyName="StrictVersion" />
    </RegexReplace>
    <AssemblyInfo CodeLanguage="CS"
      OutputFile="Properties/AssemblyInfo.cs"
      AssemblyTitle="Embed Version Sample"
      AssemblyDescription="Embed Version Sample Description"
      AssemblyConfiguration=""
      AssemblyCompany=""
      AssemblyProduct="Embed Version Sample"
      AssemblyCopyright="Copyright c 2021"
      AssemblyTrademark=""
      AssemblyCulture=""
      ComVisible="false"
      Guid="ec86a331-8ca3-4979-a09b-e96995eac5b9"
      AssemblyVersion="$(StrictVersion)"
      AssemblyFileVersion="$(StrictVersion)"
      AssemblyInformationalVersion="$(VersionTag.Substring(1)).$(CommitHash)" />
  </Target>

以下は動作確認の際に設定した*.csprojの内容になります。

ターゲット内の各記述を詳しく説明します。

Propertiesディレクトリの作成

Properties/AssemblyInfo.csを生成する際に、事前にディレクトリが存在する必要があるため、MakeDirタスクで生成します。

<MakeDir Directories="Properties" />

Gitのコミットハッシュ取得

GitVersionタスクでGitのコミットハッシュを取得します。

ShortLengthでハッシュの桁数を指定しています。

Output要素は、タスクの出力内容を指定したプロパティに出力するものです。下記だとタスクのCommitHashという出力値を、CommitHashというプロパティに出力しています。プロパティは後から$(CommitHash)といった形で参照できます。

<GitVersion ShortLength="8">
  <Output TaskParameter="CommitHash" PropertyName="CommitHash" />
</GitVersion>

GitVersionタスクはドキュメントに例が用意されていないようなのですが、ソースコードを見ると取得できる情報とオプションの内容がわかります。

Gitの直近のタグ取得

GitDescribeタスクでGitの直近のタグを取得します。Gitのdescribeコマンドの内容が取得できます。

LightWeightTrueとすると、注釈無しのタグも対象とします。git describe --tagsを指定したのと同じイメージです。
Matchで対象とするタグを指定できます。git describe --matchで指定したのと同じイメージです。ここではvから始まるタグを対象にするようにしています。

git describeでは、マージ元も含めて直近のタグを参照します。マージ元の情報は欲しくないので、git describe --first-parentの結果としたいのですが、--first-parentを切り替えるようなオプションはありません。
Commandgitコマンドの引数を指定できます。デフォルトはdescribeなのですが、そこにdescribe --first-parentとすることで、--first-parentを指定するようにしています。

最終的にgit describe --first-parent --tags --match "v[0-9]*"として指定したことになります。

<GitDescribe Command="describe --first-parent" LightWeight="True" Match="v[0-9]*">
  <Output TaskParameter="Tag" PropertyName="VersionTag" />
</GitDescribe>

GitDescribeタスクもドキュメントに例が用意されていないようなのですが、ソースコードを見ると取得できる情報とオプションの内容がわかります。

アセンブリバージョンとして利用できる形式に整形

タグの情報のままだと、アセンブリバージョンとして使用できないので、RegexReplaceタスクでアセンブリバージョンとして利用できる部分を取り出します。

v1.0.0-betaのようなタグだった場合、1.0.0の部分のみにするようなイメージです。

<RegexReplace Input="$(VersionTag)" Expression="v([0-9\.]+).*" Replacement="$1">
  <Output TaskParameter="Output" PropertyName="StrictVersion" />
</RegexReplace>

RegexReplaceタスクはドキュメントに例が記載されています。

ただ、ソースコードを見た方が理解が早いかもしれません。

AssemblyInfo.csの生成

AssemblyInfoタスクを利用して、AssemblyInfo.csを生成します。

今まで取得した情報を利用することで、現在のGitの情報を元にしたAssemblyInfo.csを生成します。

<AssemblyInfo CodeLanguage="CS"
  OutputFile="Properties/AssemblyInfo.cs"
  AssemblyTitle="Embed Version Sample"
  AssemblyDescription="Embed Version Sample Description"
  AssemblyConfiguration=""
  AssemblyCompany=""
  AssemblyProduct="Embed Version Sample"
  AssemblyCopyright="Copyright © 2021"
  AssemblyTrademark=""
  AssemblyCulture=""
  ComVisible="false"
  Guid="ec86a331-8ca3-4979-a09b-e96995eac5b9"
  AssemblyVersion="$(StrictVersion)"
  AssemblyFileVersion="$(StrictVersion)"
  AssemblyInformationalVersion="$(VersionTag.Substring(1)).$(CommitHash)" />

AssemblyInfoタスクはドキュメントに例が記載されています。

こちらもソースコードを見た方が何を指定可能なのかわかりやすかったです。

AssemblyInfo.csに必要な情報は全て設定できるようになっているように見えますが、もしも任意のコードを埋め込みたい場合には、AssemblyInfoタスクの後に、FileUpdateタスクを使ってさらに更新することでどうにかなりそうです。

AssemblyInfo.cs をバージョン管理から除外

AssemblyInfo.csはビルドのたびに生成されることになるため、Gitなどで管理してしまうと、毎回差分が発生することになりかねません。
AssemblyInfo.csを削除したうえで、バージョン管理から除外するようにします。

Gitだと.gitignoreに記載します。

(おまけ) アセンブリに埋め込んだ情報をプログラムから取得する

アセンブリに埋め込んだ情報ですが、プログラムから取得することができます。

var assembly = Assembly.GetExecutingAssembly();

Console.WriteLine($"AssemblyVersion: {assembly.GetName().Version}");

var versionInfo = FileVersionInfo.GetVersionInfo(assembly.Location);

Console.WriteLine($"AssemblyFileVersion: {versionInfo.FileVersion}");
Console.WriteLine($"AssemblyInformationalVersion: {versionInfo.ProductVersion}");

AssemblyInformationalVersionが一番多くの情報が含まれるので、AssemblyInformationalVersionを起動時のログとして出しておくと、どのバージョンで実行された時のログなのかわかりやすくなると思います。

Discussion