I'm building a C# application, using Git as my version control.
Is there a way to automatically embed the last commit hash in the executable when I build my application?
For example, printing the commit hash to console would look something like:
class PrintCommitHash
{
private String lastCommitHash = ?? // What do I put here?
static void Main(string[] args)
{
// Display the version number:
System.Console.WriteLine(lastCommitHash );
}
}
Note that this has to be done at build time, not runtime, as my deployed executable will not have the git repo accessible.
A related question for C++ can be found here.
EDIT
Per #mattanja's request, I'm posting the git hook script I use in my projects. The setup:
The hooks are linux shell scripts, which are placed under: path_to_project\.git\hooks
If you are using msysgit, the hooks folder already contains some sample scripts. In order to make git call them, remove the '.sample' extension from the script name.
The names of the hook scripts match the event that invokes them. In my case, I modified post-commit and post-merge.
My AssemblyInfo.cs file is directly under the project path (same level as the .git folder). It contains 23 lines, and I use git to generate the 24th.
As my linux-shelling a bit rusty, the script simply reads the first 23-lines of AssemblyInfo.cs to a temporary file, echos the git hash to the last line, and renames the file back to AssemblyInfo.cs. I'm sure there are better ways of doing this:
#!/bin/sh
cmt=$(git rev-list --max-count=1 HEAD)
head -23 AssemblyInfo.cs > AssemblyInfo.cs.tmp
echo [assembly: AssemblyFileVersion\(\"$cmt\"\)] >> AssemblyInfo.cs.tmp
mv AssemblyInfo.cs.tmp AssemblyInfo.cs
UPDATE:
Things have evolved since I originally answered this question. The Microsoft.NET.Sdk (meaning you must be using an sdk-style project) now includes support for adding the commit hash to both the assembly informational version as well as to the nuget package metadata, if some conditions are met:
The <SourceRevisionId> property must be defined. This can be done by adding a target like this:
<Target Name="SetSourceRevisionId" BeforeTargets="InitializeSourceControlInformation">
<Exec
Command="git describe --long --always --dirty --exclude=* --abbrev=8"
ConsoleToMSBuild="True"
IgnoreExitCode="False"
>
<Output PropertyName="SourceRevisionId" TaskParameter="ConsoleOutput"/>
</Exec>
</Target>
This target executes a command that will set SourceRevisionId to be the abbreviated (8 character) hash. The BeforeTargets causes this to be run before the assembly informational version is created.
To include the hash in the nuget package metadata, the <RepositoryUrl> must also be defined.
<SourceControlInformationFeatureSupported> property must be true, this causes the nuget pack task to pick up the SourceRevisionId as well.
I would steer people away from using the MSBuildGitHash package, since this new technique is cleaner and most consistent.
ORIGINAL:
I've created a simple nuget package that you can include in your project which will take care of this for you: https://www.nuget.org/packages/MSBuildGitHash/
This nuget package implements a "pure" MSBuild solution. If you'd rather not depend on a nuget package you can simply copy these Targets into your csproj file and it should include the git hash as a custom assembly attribute:
<Target Name="GetGitHash" BeforeTargets="WriteGitHash" Condition="'$(BuildHash)' == ''">
<PropertyGroup>
<!-- temp file for the git version (lives in "obj" folder)-->
<VerFile>$(IntermediateOutputPath)gitver</VerFile>
</PropertyGroup>
<!-- write the hash to the temp file.-->
<Exec Command="git -C $(ProjectDir) describe --long --always --dirty > $(VerFile)" />
<!-- read the version into the GitVersion itemGroup-->
<ReadLinesFromFile File="$(VerFile)">
<Output TaskParameter="Lines" ItemName="GitVersion" />
</ReadLinesFromFile>
<!-- Set the BuildHash property to contain the GitVersion, if it wasn't already set.-->
<PropertyGroup>
<BuildHash>#(GitVersion)</BuildHash>
</PropertyGroup>
</Target>
<Target Name="WriteGitHash" BeforeTargets="CoreCompile">
<!-- names the obj/.../CustomAssemblyInfo.cs file -->
<PropertyGroup>
<CustomAssemblyInfoFile>$(IntermediateOutputPath)CustomAssemblyInfo.cs</CustomAssemblyInfoFile>
</PropertyGroup>
<!-- includes the CustomAssemblyInfo for compilation into your project -->
<ItemGroup>
<Compile Include="$(CustomAssemblyInfoFile)" />
</ItemGroup>
<!-- defines the AssemblyMetadata attribute that will be written -->
<ItemGroup>
<AssemblyAttributes Include="AssemblyMetadata">
<_Parameter1>GitHash</_Parameter1>
<_Parameter2>$(BuildHash)</_Parameter2>
</AssemblyAttributes>
</ItemGroup>
<!-- writes the attribute to the customAssemblyInfo file -->
<WriteCodeFragment Language="C#" OutputFile="$(CustomAssemblyInfoFile)" AssemblyAttributes="#(AssemblyAttributes)" />
</Target>
There are two targets here. The first one, "GetGitHash", loads the git hash into an MSBuild property named BuildHash, it only does this if BuildHash is not already defined. This allows you to pass it to MSBuild on the command line, if you prefer. You could pass it to MSBuild like so:
MSBuild.exe myproj.csproj /p:BuildHash=MYHASHVAL
The second target, "WriteGitHash", will write the hash value to a file in the temporary "obj" folder named "CustomAssemblyInfo.cs". This file will contain a line that looks like:
[assembly: AssemblyMetadata("GitHash", "MYHASHVAL")]
This CustomAssemblyInfo.cs file will be compiled into your assembly, so you can use reflection to look for the AssemblyMetadata at runtime. The following code shows how this can be done when the AssemblyInfo class is included in the same assembly.
using System.Linq;
using System.Reflection;
public static class AssemblyInfo
{
/// <summary> Gets the git hash value from the assembly
/// or null if it cannot be found. </summary>
public static string GetGitHash()
{
var asm = typeof(AssemblyInfo).Assembly;
var attrs = asm.GetCustomAttributes<AssemblyMetadataAttribute>();
return attrs.FirstOrDefault(a => a.Key == "GitHash")?.Value;
}
}
Some benefits to this design is that it doesn't touch any files in your project folder, all the mutated files are under the "obj" folder. Your project will also build identically from within Visual Studio or from the command line. It can also be easily customized for your project, and will be source controlled along with your csproj file.
You can embed a version.txt file into the executable and then read the version.txt out of the executable. To create the version.txt file, use git describe --long
Here are the steps:
Use a Build Event to call git
Right-click on the project and select Properties
In Build Events, add Pre-Build event containing (notice the quotes):
"C:\Program Files\Git\bin\git.exe" describe --long > "$(ProjectDir)\version.txt"
That will create a version.txt file in your project directory.
Embed the version.txt in the executable
Right click on the project and select Add Existing Item
Add the version.txt file (change the file chooser filter to let you see All Files)
After version.txt is added, right-click on it in the Solution Explorer and select Properties
Change the Build Action to Embedded Resource
Change Copy to Output Directory to Copy Always
Add version.txt to your .gitignore file
Read the embedded text file version string
Here's some sample code to read the embedded text file version string:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Reflection;
namespace TryGitDescribe
{
class Program
{
static void Main(string[] args)
{
string gitVersion= String.Empty;
using (Stream stream = Assembly.GetExecutingAssembly()
.GetManifestResourceStream("TryGitDescribe." + "version.txt"))
using (StreamReader reader = new StreamReader(stream))
{
gitVersion= reader.ReadToEnd();
}
Console.WriteLine("Version: {0}", gitVersion);
Console.WriteLine("Hit any key to continue");
Console.ReadKey();
}
}
}
We use tags in git to track versions.
git tag -a v13.3.1 -m "version 13.3.1"
You can get the version with hash from git via:
git describe --long
Our build process puts the git hash in the AssemblyInformationalVersion attribute of the AssemblyInfo.cs file:
[assembly: AssemblyInformationalVersion("13.3.1.74-g5224f3b")]
Once you compile, you can view the version from windows explorer:
You can also get it programmatically via:
var build = ((AssemblyInformationalVersionAttribute)Assembly
.GetAssembly(typeof(YOURTYPE))
.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false)[0])
.InformationalVersion;
where YOURTYPE is any Type in the Assembly that has the AssemblyInformationalVersion attribute.
I think this question is worth giving a complete step by step answer. The strategy here to is run a powershell script from the pre-build events that takes in a template file and generates an AssemblyInfo.cs file with the git tag + commit count information included.
Step 1: make an AssemblyInfo_template.cs file in the Project\Properties folder, based on your original AssemblyInfo.cs but containing:
[assembly: AssemblyVersion("$FILEVERSION$")]
[assembly: AssemblyFileVersion("$FILEVERSION$")]
[assembly: AssemblyInformationalVersion("$INFOVERSION$")]
Step 2: Create a powershell script named InjectGitVersion.ps1 whose source is:
# InjectGitVersion.ps1
#
# Set the version in the projects AssemblyInfo.cs file
#
# Get version info from Git. example 1.2.3-45-g6789abc
$gitVersion = git describe --long --always;
# Parse Git version info into semantic pieces
$gitVersion -match '(.*)-(\d+)-[g](\w+)$';
$gitTag = $Matches[1];
$gitCount = $Matches[2];
$gitSHA1 = $Matches[3];
# Define file variables
$assemblyFile = $args[0] + "\Properties\AssemblyInfo.cs";
$templateFile = $args[0] + "\Properties\AssemblyInfo_template.cs";
# Read template file, overwrite place holders with git version info
$newAssemblyContent = Get-Content $templateFile |
%{$_ -replace '\$FILEVERSION\$', ($gitTag + "." + $gitCount) } |
%{$_ -replace '\$INFOVERSION\$', ($gitTag + "." + $gitCount + "-" + $gitSHA1) };
# Write AssemblyInfo.cs file only if there are changes
If (-not (Test-Path $assemblyFile) -or ((Compare-Object (Get-Content $assemblyFile) $newAssemblyContent))) {
echo "Injecting Git Version Info to AssemblyInfo.cs"
$newAssemblyContent > $assemblyFile;
}
Step 3: Save the InjectGitVersion.ps1 file to your solution directory in a BuildScripts folder
Step 4: Add the following line to the project's Pre-Build events
powershell -ExecutionPolicy ByPass -File $(SolutionDir)\BuildScripts\InjectGitVersion.ps1 $(ProjectDir)
Step 5: Build your project.
Step 6: Optionally, add AssemblyInfo.cs to your git ignore file
Another way to do this is to use the NetRevisionTool with some On-Board Visual Studio magic. I will showcase this here for Visual Studio 2013 Professional Edition, but this will work with other versions as well.
So first download the NetRevisionTool.
You include the NetRevisionTool.exe in your PATH or check it in into your repository and create a visual studio pre-build and a post-build action and change your AssemblyInfo.cs.
An example that would add your git-hash to your AssemblyInformationVersion would be the following:
In your project settings:
in the AssemblyInfo.cs of your project you change/add the line:
[assembly: AssemblyInformationalVersion("1.1.{dmin:2015}.{chash:6}{!}-{branch}")]
in the shown screenshot i checked in NetRevisionTool.exe in the External/bin folder
After build, if you then right-click your binary and go to properties then you should see something like the following:
Hope this helps somebody out there
It's now very easy with .NET Revision Task for MSBuild and working with Visual Studio 2019.
Simply install the NuGet package Unclassified.NetRevisionTask, then configure the information you want in theAssemblyInfo.cs file as described in the GitHub documentation.
If you only want the hash of the last commit (length=8):
[assembly: AssemblyInformationalVersion("1.0-{chash:8}")]
Build your project/solution and you'll have something like this:
As the other answer already mentions the git bit, once you have the SHA you can consider generating the AssemblyInfo.cs file of your project in a pre-build hook.
One way to do this is to create an AssemblyInfo.cs.tmpl template file, with a placeholder for your SHA in say $$GITSHA$$, e.g.
[assembly: AssemblyDescription("$$GITSHA$$")]
Your pre build hook then has to replace this placeholder and output the AssemblyInfo.cs file for the C# compiler to pick up.
To see how this can be done using SubWCRev for SVN see this answer. It shouldn't be hard to do something similar for git.
Other ways would be a "make stage" as mentioned, i.e. write an MSBuild task that does something similar. Yet another way may be to post process the DLL somehow (ildasm+ilasm say), but I think the options mentioned above are probably easiest.
For a fully automated and flexible method checkout https://github.com/Fody/Stamp. We've successfully used this for our Git projects (as well as the this version for SVN projects)
Update: This is outdated since Stamp.Fody is no longer maintained
You can use a powershell one-liner to update all assemblyinfo files with the commit hash.
$hash = git describe --long --always;gci **/AssemblyInfo.* -recurse | foreach { $content = (gc $_) -replace "\[assembly: Guid?.*", "$&`n[assembly: AssemblyMetadata(`"commithash`", `"$hash`")]" | sc $_ }
Here is a simple solution that works in Visual Studio 2019 and gets the git commit hash directly into the C# file. Add the following C# code to your solution:
namespace MyNameSpace
{
[System.AttributeUsage(System.AttributeTargets.Assembly, Inherited = false, AllowMultiple = false)]
sealed class GitHashAttribute : System.Attribute
{
public string Hash { get; }
public GitHashAttribute(string hsh)
{
this.Hash = hsh;
}
}
var hash = Assembly.GetEntryAssembly().GetCustomAttribute<GitHashAttribute>().Hash;
}
Variable hash is then going to contain the desired string if you add the following lines to your .csproj file.
<Target Name="SetSourceRevisionId" BeforeTargets="InitializeSourceControlInformation">
<Exec Command="git.exe describe --long --always --dirty --exclude='*' --abbrev=40"
ConsoleToMSBuild="True" IgnoreExitCode="False">
<Output PropertyName="SourceRevisionId" TaskParameter="ConsoleOutput" />
</Exec>
</Target>
<Target Name="SetHash" AfterTargets="InitializeSourceControlInformation">
<ItemGroup>
<AssemblyAttribute Include="MyNameSpace.GitHashAttribute">
<_Parameter1>$(SourceRevisionId)</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Target>
Make sure than MyNameSpace in both files matches your demand.
The important point here is that the ItemGroup has to be embedded into a Target with appropriate AfterTargets set.
I hope you know how to call external programs and intercept output at the build-time.
I hope you know how to have in git's working directory ignore unversioned files.
As noted by #learath2, output of git rev-parse HEAD will give you plain hash.
If you use tags in Git-repository (and you use tags, isn't it more descriptive and readable than git rev-parse), output may be received from git describe (while also successfully used later in git checkout)
You can call rev-parse|describe in:
some make stage
in post-commit hook
in smudge filter, if you'll select smudge/clean filters way of implementation
Another way would be to generate a Version.cs file from a Pre-Build step. I explored this in a little proof-of-concept project which prints out its current commit hash.
Tha project is uploaded on https://github.com/sashoalm/GitCommitHashPrinter.
The batch code which creates the Version.cs file is this:
#echo off
echo "Writing Version.cs file..."
#rem Pushd/popd are used to temporarily cd to where the BAT file is.
pushd $(ProjectDir)
#rem Verify that the command succeeds (i.e. Git is installed and we are in the repo).
git rev-parse HEAD || exit 1
#rem Syntax for storing a command's output into a variable (see https://stackoverflow.com/a/2340018/492336).
#rem 'git rev-parse HEAD' returns the commit hash.
for /f %%i in ('git rev-parse HEAD') do set commitHash=%%i
#rem Syntax for printing multiline text to a file (see https://stackoverflow.com/a/23530712/492336).
(
echo namespace GitCommitHashPrinter
echo {
echo class Version
echo {
echo public static string CommitHash { get; set; } = "%commitHash%";
echo }
echo }
)>"Version.cs"
popd
Open the .csproj and add <GenerateAssemblyInfo>false</GenerateAssemblyInfo> to the first PropertyGroup
You may want to copy the contents of the already generated AssemblyInfo.cs in the obj folder so you don't have to write everything yourself.
Create AssemblyInfo.tt (T4 template) in the properties folder.
Paste the following contents + the old contents of your previously auto generated AssemblyInfo.cs
<## template debug="true" hostspecific="True" language="C#" #>
<## assembly name="System.Core" #>
<# /*There's a bug with VS2022 where you have to be real specific about envDTE.*/ #>
<## assembly name="./PublicAssemblies/envdte.dll" #>
<## import namespace="System.IO" #>
<## import namespace="System.Text.RegularExpressions" #>
<## import namespace="System.Globalization" #>
<## output extension=".cs" #>
<#
var dte = ((IServiceProvider)this.Host).GetService(typeof(EnvDTE.DTE)) as EnvDTE.DTE;
string buildConfig = dte.Solution.SolutionBuild.ActiveConfiguration.Name;
string solutionDirectory = Path.GetDirectoryName(dte.Solution.FullName);
var (gitRevision, gitBranch, gitCompactRevision) = ("", "", "");
using(var process = new System.Diagnostics.Process() {
StartInfo = new System.Diagnostics.ProcessStartInfo() {
WorkingDirectory = solutionDirectory,
FileName = #"cmd.exe",
Arguments = "/C git rev-parse HEAD & git rev-parse --abbrev-ref HEAD",
RedirectStandardError = true,
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
}
}) {
process.Start();
string[] lines = process.StandardOutput.ReadToEnd().Split();
gitRevision = lines[0].Trim();
gitBranch = lines[1].Trim();
gitCompactRevision = gitRevision.Substring(0, 6);
}
string appPurpose = "Launcher"; // & Updater
string companyShort = "todo";
string companyFull = "todo";
string productNameShort = "todo";
string productName = $"{companyShort} {productNameShort}";
string fileName = $"{companyShort}{productNameShort}";
string exeNAME = $"{fileName}Launch";
string originalFilename = $"{exeNAME}.exe";
string CALLEXE = $"{fileName}.exe";
string BROWSEREXE = $"{fileName}Browser.exe";
string FULLINSTALLER = $"{fileName}Setup.exe";
DateTime dtBuiltDate = DateTime.UtcNow;
string cBuildYear = dtBuiltDate.Year.ToString();
string cBuildDay = dtBuiltDate.ToString("dd");
string cBuildMonth = dtBuiltDate.ToString("MM");
string cBuildTime = dtBuiltDate.ToString("T", DateTimeFormatInfo.InvariantInfo);
string assemblyVersion = $"3.0.{cBuildYear}.{cBuildMonth}{cBuildDay}";
string JOB_NAME = System.Environment.GetEnvironmentVariable("JOB_NAME") ?? "0.0";
string buildVersion = System.Environment.GetEnvironmentVariable("BUILD_NUMBER") ?? "0-dev";
string buildSeries = Regex.Replace(JOB_NAME, #"[^0-9\.]+", "");
string buildNumber = Regex.Replace(buildVersion, #"[^0-9\.]+", "");
string InternalVersion = $"{JOB_NAME}.{buildVersion}";
string fileVersion = Regex.Replace(InternalVersion, #"[^0-9\.]+", "");
#>
using System.Reflection;
[assembly: System.Runtime.InteropServices.ComVisible(false)]
[assembly: System.Resources.NeutralResourcesLanguageAttribute("en")]
[assembly: AssemblyConfigurationAttribute("<#= buildConfig #>")]
[assembly: AssemblyProduct("<#= productName #>")]
[assembly: AssemblyTitle("<#= $"{companyShort}{productNameShort}" #>")]
[assembly: AssemblyCompany("<#= companyFull #>")]
[assembly: AssemblyDescription("<#= $"{companyShort} {productNameShort} .... {appPurpose} - ...... by {companyFull}" #>")]
[assembly: AssemblyCopyright("<#= $"© 1983-{cBuildYear} {companyFull}" #>")]
[assembly: AssemblyTrademark("<#= $"{productName} is a trademark of {companyFull}, Inc." #>")]
[assembly: AssemblyInformationalVersion("<#= InternalVersion #>")]
[assembly: AssemblyVersion("<#= assemblyVersion #>")]
[assembly: AssemblyFileVersion("<#= fileVersion #>")]
[assembly: AssemblyMetadataAttribute("OriginalFilename", "<#= originalFilename #>")]
[assembly: AssemblyMetadataAttribute("NAME", "<#= $"{productName} {appPurpose}" #>")]
[assembly: AssemblyMetadataAttribute("EXENAME", "<#= exeNAME #>")]
[assembly: AssemblyMetadataAttribute("DIRNAME", "<#= productNameShort #>")]
[assembly: AssemblyMetadataAttribute("CALLEXE", "<#= $"{fileName}.exe" #>")]
[assembly: AssemblyMetadataAttribute("BROWSEREXE", "<#= $"{fileName}Browser.exe" #>")]
[assembly: AssemblyMetadataAttribute("FULLINSTALLER", "<#= $"{fileName}Setup.exe" #>")]
[assembly: AssemblyMetadataAttribute("COMPANY", "<#= companyFull #>")]
[assembly: AssemblyMetadataAttribute("License", "<#= $"Contains copyrighted code and applications ..." #>")]
[assembly: AssemblyMetadataAttribute("TermsOfUse", "<#= "https://www.company.com/en-us/terms-of-use/" #>")]
[assembly: AssemblyMetadataAttribute("Website", "<#= "https://www.company.com/en-us" #>")]
[assembly: AssemblyMetadataAttribute("UpdateURL", "https://subdomain.product.net/version_check")]
[assembly: AssemblyMetadataAttribute("BuildYear", "<#= cBuildYear #>")]
[assembly: AssemblyMetadataAttribute("BuildDay", "<#= cBuildDay #>")]
[assembly: AssemblyMetadataAttribute("BuildMonth", "<#= cBuildMonth #>")]
[assembly: AssemblyMetadataAttribute("BuildTime", "<#= cBuildTime #>")]
[assembly: AssemblyMetadataAttribute("DateModified", "<#= $"{dtBuiltDate.ToString("MMM dd, yyyy", DateTimeFormatInfo.InvariantInfo)} at {cBuildTime}" #>")]
[assembly: AssemblyMetadataAttribute("BuildSeries", "<#= buildSeries #>")]
[assembly: AssemblyMetadataAttribute("BuildNumber", "<#= buildNumber #>")]
[assembly: AssemblyMetadataAttribute("BuildDate", "<#= dtBuiltDate.ToString("s") #>")]
[assembly: AssemblyMetadataAttribute("BuildMachine", "<#= Environment.MachineName #>")]
[assembly: AssemblyMetadataAttribute("BuildMachineUser", "<#= Environment.UserName #>")]
[assembly: AssemblyMetadataAttribute("BuildOSVersion", "<#= Environment.OSVersion #>")]
[assembly: AssemblyMetadataAttribute("BuildPlatform", "<#= Environment.OSVersion.Platform #>")]
[assembly: AssemblyMetadataAttribute("BuildClrVersion", "<#= Environment.Version #>")]
[assembly: AssemblyMetadataAttribute("BuildBranch", "<#= gitBranch #>")]
[assembly: AssemblyMetadataAttribute("BuildRevision", "<#= gitCompactRevision #>")]
[assembly: AssemblyMetadataAttribute("CommitHash", "<#= gitRevision #>")]
[assembly: AssemblyMetadataAttribute("RepositoryUrl", "")]
[assembly: AssemblyMetadataAttribute("RepositoryType", "")]
<#+
#>
You can now use the full power of C# to generate whatever you want, such as the git branch and revision you're currently on. Some tips:
Variables can be declared anywhere inside a <# #> block
Any methods you wish to use must be declared at the end of the file in a <#+ #> block. (The + sign is very important and it must be the last thing at the end of the file))
Everything outside of <# #> blocks is just plain text.
VS2019 has no syntax highlighting or intellisense. The .tt file is plain text. I recommend editing it with vscode after installing T4 Support extension (not available in vs2019...)
I'm using a combination of the accepted answer and a small adition.
I have th AutoT4 extension installed (https://marketplace.visualstudio.com/items?itemName=BennorMcCarthy.AutoT4) to re-run the templates before build.
getting version from Git
I have git -C $(ProjectDir) describe --long --always > "$(ProjectDir)git_version.txt" in my pre-build event in project properties.
Adding git_version.txt and VersionInfo.cs to .gitignore is quite a good idea.
embedding version in metadata
I have added a VersionInfo.tt template to my project:
<## template debug="false" hostspecific="true" language="C#" #>
<## assembly name="System.Core" #>
<## import namespace="System.Linq" #>
<## import namespace="System.Text" #>
<## import namespace="System.Collections.Generic" #>
<## import namespace="System.IO" #>
<## output extension=".cs" #>
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
<#
if (File.Exists(Host.ResolvePath("git_version.txt")))
{
Write("[assembly: AssemblyInformationalVersion(\""+ File.ReadAllText(Host.ResolvePath("git_version.txt")).Trim() + "\")]");
}else{
Write("// version file not found in " + Host.ResolvePath("git_version.txt"));
}
#>
Now I have my git tag + hash in "ProductVersion".
Referring to the another answer (https://stackoverflow.com/a/44278482/4537127) i also utilised the VersionInfo.tt text template to generate AssemblyInformationalVersion without AutoT4.
(Atleast works in my C# WPF application)
Problem was that the Pre-build events were run after template transformations, so after cloning, the git_version.txt file was not there and build fails.
After creating it manually to allow transformation to pass once, it was updated after transformation, and was always one commit behind.
I had to make two adjustments to the .csproj file (this applies at least for Visual Studio Community 2017)
1) Import the Text Transformation Targets and make template transformations to run on every build: (Ref https://msdn.microsoft.com/en-us/library/ee847423.aspx)
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">15.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<TransformOnBuild>true</TransformOnBuild>
<TransformOutOfDateOnly>false</TransformOutOfDateOnly>
</PropertyGroup>
and after <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(VSToolsPath)\TextTemplating\Microsoft.TextTemplating.targets" />
2) Make the git describe run before template transformations (so that git_version.txt is there when VersionInfo.tt is transformed) :
<Target Name="PreBuild" BeforeTargets="ExecuteTransformations">
<Exec Command="git -C $(ProjectDir) describe --long --always --dirty > $(ProjectDir)git_version.txt" />
</Target>
..And the C# code to get the AssemblyInformationalVersion (Ref https://stackoverflow.com/a/7770189/4537127)
public string AppGitHash
{
get
{
AssemblyInformationalVersionAttribute attribute = (AssemblyInformationalVersionAttribute)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false).FirstOrDefault();
return attribute.InformationalVersion;
}
}
..And add the generated files to .gitignore
VersionInfo.cs
git_version.txt
Place
<Target Name="UpdateVersion" BeforeTargets="CoreCompile">
<Exec Command="php "$(SolutionDir)build.php" $(SolutionDir) "$(ProjectDir)Server.csproj"" />
</Target>
in YOUR_PROJECT_NAME.csproj
<?php
function between(string $string, string $after, string $before, int $offset = 0) : string{
return substr($string, $pos = strpos($string, $after, $offset) + strlen($after),
strpos($string, $before, $pos) - $pos);
}
$pipes = [];
$proc = proc_open("git rev-parse --short HEAD", [
0 => ["pipe", "r"],
1 => ["pipe", "w"],
2 => ["pipe", "w"]
], $pipes, $argv[1]);
if(is_resource($proc)){
$rev = stream_get_contents($pipes[1]);
proc_close($proc);
}
$manifest = file_get_contents($argv[2]);
$version = between($manifest, "<Version>", "</Version>");
$ver = explode("-", $version)[0] . "-" . trim($rev);
file_put_contents($argv[2], str_replace($version, $ver, $manifest));
echo "New version generated: $ver" . PHP_EOL;
Heavily inspired by #John Jesus answer, I've created a Powershell v1 script that runs on each Build to adjust the Assembly Version to the current Git tag.
The Powershell script
# Get build running directory
$scriptPath = split-path -parent $MyInvocation.MyCommand.Path
try {
$v = git describe --tags
}
catch [System.Management.Automation.CommandNotFoundException] {
# Git not found
exit
}
# Letters are incompatible with AssemblyVersion.cs so we remove them
$v = $v -replace "v", ""
# Version format is major[.minor[.build[.revision]] so we remove them
$v = $v -replace "-(\D.*)", ''
$v = $v -replace "-", '.'
# We replace versions inside AssemblyInfo.cs content
$info = (Get-Content ($scriptPath + "/properties/AssemblyInfo.cs"))
$av = '[assembly: AssemblyVersion("'+$v+'")]'
$avf = '[assembly: AssemblyFileVersion("'+$v+'")]'
$info = $info -replace '\[assembly: AssemblyVersion\("(.*)"\)]', $av
$info = $info -replace '\[assembly: AssemblyFileVersion\("(.*)"\)]', $avf
Set-Content -Path ($scriptPath + "/properties/AssemblyInfo.cs") -Value $info -Encoding UTF8
Place it your solution folder and set a Prebuild Event to launch it:
I deploy these files on our dev/staging systems to have a quick look:
git.exe -C "$(ProjectDir.TrimEnd('\'))" describe --long > "$(ProjectDir)_Version.info":
MyResult: 10.02.0.3-247-gbeeadd082
git.exe -C "$(ProjectDir.TrimEnd('\'))" branch --show-current > "$(ProjectDir)_Branch.info"
MyResult: feature/JMT-3931-jaguar
(Visual Studio PreBuild Events)
Only one line, add reference to Microsoft.SourceLink.GitHub
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
and git commit hash is auto imbedded to .Net dll.
For more details read docs
The more convenient way I found it to add
<ItemGroup>
<PackageReference Include="GitVersion.MsBuild" Version="5.10.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
to .csproj. This builds on top of the very flexible GitVersion tool.
This sets the Product Version in the produced dll. You can control which information to be output by creating a GitVersion.yml file containing
assembly-informational-format: '{Sha}'
Other formats are available. They are listed in the Version Variables in the GitVersion documentation.
Another option is to use SourceLink, which is provided by Microsoft itself. Honestly, though, I found a great deal of quirks and limitations with this approach (e.g. it seems SourceLinks wants to know where the repo is hosted, and must configured differently depending on it is on Azure DevOps, GitHub, GitLab, BitBucket, gitea etc).
Related
How do you access publish information for use in a T4 Text Template?
For example, I want to create a text template that generates an xml file that will be published with an ASP.Net Core MVC website. The generated file should be different depending on where I publish the website; Production or Test environment. So I would have something like this in the .tt file such that when it is generated it varies depending on the selected publish profile or the publish path:
<#
if(publishing to A)
{
#>
text output specific to A
<#
}
else if(publishing to B)
{
#>
text output specific to B
<#
}
#>
EDIT:
I just found this and it looks promising:
using-msbuild-properties-in-t4-templates
This MSDN blog by Jeremy Kuhne and this blog by Thomas Levesque and several other links such as this MSDN doc helped get it working in VS2017.
I did not have to add anything to the beginning of the .csproj file since VS2017 has the files already included by default.
In Visual Studio 2017, the Text Template Transformation component is
automatically installed as part of the Visual Studio extension
devlopment workload. You can also install it from the Individual
components tab of Visual Studio Installer, under the Code tools
category. Install the Modeling SDK component from the Individual
components tab.
I ended up with the following .csproj changes at the end of the file. This will allow the selected build configuration to be availed in the T4 template and cause all templates to be regenerated on each build:
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">15.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<!-- Run the Transform task at the start of every build -->
<TransformOnBuild>true</TransformOnBuild>
<!-- -->
<OverwriteReadOnlyOutputFiles>true</OverwriteReadOnlyOutputFiles>
<!-- Transform every template every time -->
<TransformOutOfDateOnly>false</TransformOutOfDateOnly>
</PropertyGroup>
<!-- add AFTER import for $(MSBuildToolsPath)\Microsoft.CSharp.targets -->
<Import Project="$(VSToolsPath)\TextTemplating\Microsoft.TextTemplating.targets" />
<ItemGroup>
<T4ParameterValues Include="BuildConfiguration">
<Value>$(Configuration)</Value>
<Visible>False</Visible>
</T4ParameterValues>
</ItemGroup>
<Target Name="CreateT4ItemListsForMSBuildCustomTool" BeforeTargets="CreateT4ItemLists" AfterTargets="SelectItemsForTransform">
<ItemGroup>
<T4Transform Include="#(CreateT4ItemListsInputs)" Condition="'%(CreateT4ItemListsInputs.Generator)' == 'MSBuild:TransformAll'" />
</ItemGroup>
</Target>
This is what is at the top of the csproj file but it can be configured through VS2017. The key points are the custom build configuration named Development and the defined constant of DEVELOPMENT:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Configurations>Debug;Release;Development</Configurations>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Development|AnyCPU'">
<DebugType>none</DebugType>
<DefineConstants>TRACE;DEVELOPMENT</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>full</DebugType>
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DebugType>none</DebugType>
<DefineConstants>TRACE;RELEASE</DefineConstants>
</PropertyGroup>
This in the T4 Template to show how to access the new BuildConfiguration parameter:
<## template hostspecific="true" language="C#" #>
<## output extension=".txt" #>
<## assembly name="EnvDTE" #>
<#
//Build time.
string configName = Host.ResolveParameterValue("-", "-", "BuildConfiguration");
if (string.IsNullOrWhiteSpace(configName))
{
try
{
//Design time.
var serviceProvider = (IServiceProvider)Host;
EnvDTE.DTE dte = (EnvDTE.DTE)serviceProvider.GetService(typeof(EnvDTE.DTE));
configName = dte.Solution.SolutionBuild.ActiveConfiguration.Name;
}
catch(Exception ex)
{
configName = ex.Message;
}
}
#>
<#=configName#>
The following property settings on the .tt file:
Build Action: None
Copy to Output Directory: Do Not Copy
Custom Tool: MSBuild:TransformAll
And a custom build configuration named "Development". The code in the T4 template will pick up "Debug", "Release" and "Development". The Development build configuration is a copy of the Release configuration. The project has a Conditional Compilation Symbol of "DEVELOPMENT" so that the following code works in Program.cs to force the environment into Development mode. The Symbol can be set under Project Properties > Build > General. The publish profile is set to publish to the test server URL with the Development build configuration.
public static void Main(string[] args)
{
//https://andrewlock.net/how-to-set-the-hosting-environment-in-asp-net-core/
string mode = "";
#if DEVELOPMENT
mode = "DEVELOPMENT";
#elif DEBUG
mode = "DEBUG";
#elif RELEASE
mode = "RELEASE";
#endif
switch (mode.ToUpper())
{
case "DEVELOPMENT":
//Programmatically force the application to use the Development environment.
CreateWebHostBuilder(args).UseEnvironment("Development").Build().Run();
break;
default:
CreateWebHostBuilder(args).Build().Run();
break;
}
}
Tools
MSBuild v14
Visual Studio 2013
Jenkins v2.111 running on Windows Server 2012
Git (bare repo on local file server)
Windows Batch
My goal
Build a c# Visual Studio project using MSBuild that pulls back the major and minor version numbers from the projects AssemblyInfo.cs for use during the build. The build would produce something like 1.2.$BUILD_NUMBER resulting in something like 1.2.121, 1.2.122, 1.2.123 and so on. Once the user opts to 'release' the build, a clickonce deployment with correct version in the folder name is copied to its target destination and a tag applied to the Git repository.
Pipeline example
Below is a 'work in progress' of what I've got up to. Any suggestions to improve are welcome. For those that are wondering why I'm coping the codebase out to a temporary folder. I'm using a multi-branch job in Jenkins and the folders that are auto-generated are extremely long! This gave me errors along the lines that my file name, project name or both are too long (because the entire path is above the 255 or so character length). So the only way to get around this was to copy out contents so the build and publish would work.
pipeline {
agent none
stages {
stage ('Checkout'){
agent any
steps
{
checkout scm
}
}
stage ('Nuget Restore'){
agent any
steps
{
bat 'nuget restore "%WORKSPACE%\\src\\Test\\MyTestSolution.sln"'
}
}
stage('Build Debug') {
agent any
steps
{
bat "xcopy %WORKSPACE%\\src\\* /ey d:\\temp\\"
bat "\"${tool 'MSBuild'}\" d:\\temp\\Test\\MyTestSolution.sln /p:Configuration=Debug /target:publish /property:PublishUrl=d:\\temp\\ /p:OutputPath=d:\\temp\\build\\ /p:GenerateBootstrapperSdkPath=\"C:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v8.1A\\Bootstrapper\" /p:VersionAssembly=1.0.$BUILD_NUMBER /p:ApplicationVersion=1.0.$BUILD_NUMBER"
}
}
stage('Deploy to Dev'){
agent none
steps {
script {
env.DEPLOY_TO_DEV = input message: 'Deploy to dev?',
parameters: [choice(name: 'Deploy to dev staging area?', choices: 'no\nyes', description: 'Choose "yes" if you want to deploy this build')]
}
}
}
stage ('Deploying to Dev')
{
agent any
when {
environment name: 'DEPLOY_TO_DEV', value: 'yes'
}
steps {
echo 'Deploying debug build...'
}
}
stage ('Git tagging')
{
agent any
steps
{
bat 'd:\\BuildTargets\\TagGit.bat %WORKSPACE% master v1.0.%BUILD_NUMBER%.0(DEV) "DEV: Build deployed."'
}
}
}
}
At the moment I've hard coded the major and minor version in the above script. I want to pull these values out of the AssemblyInfo.cs so that developers can control it from there without editing the Jenkinsfile. Any suggestions/best practice to achieve this?
Because I'm doing a clickonce deployment for a winforms app I've had to use MSBuild's VersionAssembly and ApplicationVersion switches to pass in the version. This seems to help with correctly labelling folders when MSBuild publishes the files. Have I have missed something in my setup which would negate these switches and make life simpler?
The last action in my pipeline is to trigger a .bat file to add a tag back into the master branch of the repository. This is another reason that I need to make the major and minor version accessible to the pipeline script.
MSBuild target for editing AssemblyInfo.cs
This code was taken from here: http://www.lionhack.com/2014/02/13/msbuild-override-assembly-version/
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<CompileDependsOn>
CommonBuildDefineModifiedAssemblyVersion;
$(CompileDependsOn);
</CompileDependsOn>
</PropertyGroup>
<Target Name="CommonBuildDefineModifiedAssemblyVersion" Condition="'$(VersionAssembly)' != ''">
<!-- Find AssemblyInfo.cs or AssemblyInfo.vb in the "Compile" Items. Remove it from "Compile" Items because we will use a modified version instead. -->
<ItemGroup>
<OriginalAssemblyInfo Include="#(Compile)" Condition="%(Filename) == 'AssemblyInfo' And (%(Extension) == '.vb' Or %(Extension) == '.cs')" />
<Compile Remove="**/AssemblyInfo.vb" />
<Compile Remove="**/AssemblyInfo.cs" />
</ItemGroup>
<!-- Copy the original AssemblyInfo.cs/.vb to obj\ folder, i.e. $(IntermediateOutputPath). The copied filepath is saved into #(ModifiedAssemblyInfo) Item. -->
<Copy SourceFiles="#(OriginalAssemblyInfo)"
DestinationFiles="#(OriginalAssemblyInfo->'$(IntermediateOutputPath)%(Identity)')">
<Output TaskParameter="DestinationFiles" ItemName="ModifiedAssemblyInfo"/>
</Copy>
<!-- Replace the version bit (in AssemblyVersion and AssemblyFileVersion attributes) using regular expression. Use the defined property: $(VersionAssembly). -->
<Message Text="Setting AssemblyVersion to $(VersionAssembly)" />
<RegexUpdateFile Files="#(ModifiedAssemblyInfo)"
Regex="Version\("(\d+)\.(\d+)(\.(\d+)\.(\d+)|\.*)"\)"
ReplacementText="Version("$(VersionAssembly)")"
/>
<!-- Include the modified AssemblyInfo.cs/.vb file in "Compile" items (instead of the original). -->
<ItemGroup>
<Compile Include="#(ModifiedAssemblyInfo)" />
</ItemGroup>
</Target>
<UsingTask TaskName="RegexUpdateFile" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<Files ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
<Regex ParameterType="System.String" Required="true" />
<ReplacementText ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Reference Include="System.Core" />
<Using Namespace="System" />
<Using Namespace="System.IO" />
<Using Namespace="System.Text.RegularExpressions" />
<Using Namespace="Microsoft.Build.Framework" />
<Using Namespace="Microsoft.Build.Utilities" />
<Code Type="Fragment" Language="cs">
<![CDATA[
try {
var rx = new System.Text.RegularExpressions.Regex(this.Regex);
for (int i = 0; i < Files.Length; ++i)
{
var path = Files[i].GetMetadata("FullPath");
if (!File.Exists(path)) continue;
var txt = File.ReadAllText(path);
txt = rx.Replace(txt, this.ReplacementText);
File.WriteAllText(path, txt);
}
return true;
}
catch (Exception ex) {
Log.LogErrorFromException(ex);
return false;
}
]]>
</Code>
</Task>
</UsingTask>
</Project>
Git tagging
This bat file is kicked off and passed values used to create and push a tag to the defined repository.
echo off
set gitPath=%1
set gitBranchName=%2
set gitTag=%3
set gitMessage=%4
#echo on
#echo Adding tag to %gitBranchName% branch.
#echo Working at path %gitPath%
#echo Tagging with %gitTag%
#echo Using commit message: %gitMessage%
d:
cd %gitPath%
git checkout %gitBranchName%
git pull
git tag -a %gitTag% -m %gitMessage%
git push origin %gitBranchName% %gitTag%
If there are any other gold nuggests that would help streamline or improve this overall workflow, would welcome those too!
I recently had the same problem which i solved by creating a Windows Script.
for /f delims^=^"^ tokens^=2 %%i in ('findstr "AssemblyFileVersion" %1\\AssemblyFile.cs') DO SET VERSION=%%i
This script extracts the version number from the AssemblyInfo.cs and put it inside an variable so it can be used later to tag the commit (in the same step though) :
CALL FindAssemblyVersion .\Properties
git tag %VERSION%
git push http://%gitCredentials%#url:port/repo.git %VERSION%
Not exactly from the assembly file but a very handy workaround to get the file version from the DLL while working with Jenkins, and using batch (or powershell) command:
Goto the directory where your DLL exists [CD Foo/Bar ]
FOR /F "USEBACKQ" %F IN (`powershell -NoLogo -NoProfile -Command (Get-Item "myApi.dll").VersionInfo.FileVersion`) DO (SET fileVersion=%F )
echo File version: %fileVersion%
How to set current year in AssemblyInfo file?
I used
Instead of this:
<Assembly: AssemblyCopyright("Copyright 2012, Company Name.")>
tried this:
<Assembly: AssemblyCopyright("Copyright" + DateTime.Now.Year.ToString() + ", Company Name.")>
I get invalid constant error.
I don't want to use registry key entries, what is the optimum way of doing this? (so that when a user right clicks on EXE & looks for assembly information can see current year).
Thanks.
I saw something on another post (https://stackoverflow.com/a/827209/857307) that could really be helpful here.
Try this.
Add a new file to your source repository somewhere common to all of the projects in your solution. Call the file something like AssemblyCopyright.tt
In the file, add the following code for c#
<## template language="C#" #>
using System;
using System.Reflection;
[assembly: AssemblyCopyright("Copyright © CompanyName <#=DateTime.Now.Year#>")]
or vb
<## template language="VB" #>
<## output extension=".vb" #>
Imports System
Imports System.Reflection
<Assembly: AssemblyCopyright("Copyright © CompanyName <#=DateTime.Now.Year#>")>
Then, remove the AssemblyCopyright attribute from each of your AssemblyInfo.cs files.
Finally, add a link to the AssemblyCopyright.tt file to each of your projects.
The template file will recreate a new AssemblyCopyright.cs file on every build with the correct year.
Try this:
<Assembly: AssemblyCopyright("Copyright $([System.DateTime]::Now.ToString('yyyy')), Company Name.")>
I haven't tried this exactly. Since i define some other parts of the assembly info via the .csproj using a WriteCodeFragment as described in this answer, I do it this way too - I define properties
<!-- We started in 2021. So, if it's still 2021 we just want to show '2021' -->
<PropertyGroup Condition="$([System.DateTime]::Now.ToString('yyyy')) == '2021'">
<CopyrightYears>2021</CopyrightYears>
</PropertyGroup>
<!-- But for later years we want a range. E.g. in 2023 it will show '2021 - 2023' -->
<PropertyGroup Condition="$([System.DateTime]::Now.ToString('yyyy')) != '2021'">
<CopyrightYears>2021 - $([System.DateTime]::Now.ToString('yyyy'))</CopyrightYears>
</PropertyGroup>
Then, within the task that updates the assembly info i have an ItemGroup including
<AssemblyAttributes Include="AssemblyCopyright">
<_Parameter1>"Copyright © $(CopyrightYears) ..."</_Parameter1>
</AssemblyAttributes>
The after the ItemGroup I have the following within that task
<MakeDir Directories="$(IntermediateOutputPath)" />
<WriteCodeFragment Language="C#"
OutputFile="$(IntermediateOutputPath)Version.cs"
AssemblyAttributes="#(AssemblyAttributes)" />
<ItemGroup>
<Compile Include="$(IntermediateOutputPath)Version.cs" />
</ItemGroup>
The functions you can use as properties are documented here
Probably the best way is to integrate this into your build process using tools like NAnt or MSBuild.
Here is an article that explains how to change your AssemblyInfo using MSBuild:
Updating Assemblies with A Version Number.
You can use NANT/MSBuild tasks to modify the AssemblyInfo.cs file like we do to change the Version of each assembly for every build.
For more information, visit http://msbuildtasks.tigris.org/
Typically, for that kind of substitution, you use a pre-build step that invokes a script that automatically generates your assemblyInfo.cs file.
You can have a look at this thread : How can you find and replace text in a file using the Windows command-line environment?
I had similar question, but for new style csproj file; that contains the file version info. I found a great answer here How to define current system date in post build event. Oddly, that is about a question that is somewhat different than my and the OP question. But, I think the accepted answer to that is what I want. Namely, $([System.DateTime]::Now.Year) in the csproj files evaluates to the current year.
Here's my solution using a prebuild task. Should work on windows and unix. Will work with dotnet (if you run dotnet build, before dotnet run).
Add this to .csproj
<!-- update assembly info -->
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Condition="$(OS) == Windows_NT" Command="set TargetDir=$(TargetDir)
..\scripts\updateAssemblyInfo.cmd" />
<Exec Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)))' OR '$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))'" Command="export TargetDir=$(TargetDir)
bash ../scripts/updateAssemblyInfo.sh" />
</Target>
AssemblyInfo.base.txt
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: AssemblyTrademark("")]
[assembly: AssemblyCopyrightAttribute("Copyright © 2009 - <#=DateTime.Now.Year#> COMPANYNAME. All rights reserved.")]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("xxx")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// [assembly: AssemblyVersion("1.0.0.0")]
// [assembly: AssemblyFileVersion("1.0.0.0")]
in scripts folder
updateAssemblyInfo.cmd
for /f %%i in ('"powershell (Get-Date).ToString(\"yyyy\")"') do set currentYear=%%i
echo %currentYear%
powershell -Command "(gc 'AssemblyInfo.base.txt' -encoding "UTF8") -replace '<#=DateTime.Now.Year#>', '%currentYear%' | Out-File -encoding "UTF8" 'AssemblyInfo.cs'"
updateAssemblyInfo.sh
#!/bin/bash
year=$(date +%Y)
echo "Current Year : $year"
contents=$(cat AssemblyInfo.base.txt)
# echo $contents
echo $contents | awk -v yr=$year '{gsub("<#=DateTime.Now.Year#>",yr); print}' > AssemblyInfo.cs
I'm getting used to using nant for build releases. But I have started to use asp.net MVC, and i choice make the setup for installation with a .vdproj .
But, when I call the:
< exec program="${dotnet.dir}/msbuild.exe" commandline='"./Wum.sln" /v:q /nologo /p:Configuration=Release' />
in nant, my result is:
[exec] D:\My Documents\Visual Studio 2008\Projects\Wum\Wum.sln : warning MS
B4078: The project file "Wum.Setup\Wum.Setup.vdproj" is not supported by MSBuild
and cannot be built.
Someone have some clue, or a solution?
If I use the devenv, I'll have a problem?
Here is how I did this recently using devenv as msbuild will not do .vdproj files. This article really helped with updating the .vdproj file first: http://www.hanselman.com/blog/BuildingMSIFilesFromNAntAndUpdatingTheVDProjsVersionInformationAndOtherSinsOnTuesday.aspx
<target name="someTarget">
<!-- full path to setup project and project file (MSI -> vdproj) -->
<property name="DeploymentProjectPath" value="fullPath\proj.vdproj" />
<!-- full path to source project and project file (EXE -> csproj) -->
<property name="DependencyProject" value="fullPath\proj.csproj" />
<script language="C#">
<code>
<![CDATA[
public static void ScriptMain(Project project)
{
//Purpose of script: load the .vdproj file, replace ProductCode and PackageCode with new guids, and ProductVersion with 1.0.SnvRevisionNo., write over .vdproj file
string setupFileName = project.Properties["DeploymentProjectPath"];
string productVersion = string.Format("1.0.{0}", project.Properties["svn.revision"]);
string setupFileContents;
//read in the .vdproj file
using (StreamReader sr = new StreamReader(setupFileName))
{
setupFileContents = sr.ReadToEnd();
}
if (!string.IsNullOrEmpty(setupFileContents))
{
Regex expression2 = new Regex(#"(?:\""ProductCode\"" = \""8.){([\d\w-]+)}");
Regex expression3 = new Regex(#"(?:\""PackageCode\"" = \""8.){([\d\w-]+)}");
Regex expression4 = new Regex(#"(?:\""ProductVersion\"" = \""8.)(\d.\d.\d+)");
setupFileContents = expression2.Replace(setupFileContents,"\"ProductCode\" = \"8:{" + Guid.NewGuid().ToString().ToUpper() + "}");
setupFileContents = expression3.Replace(setupFileContents,"\"PackageCode\" = \"8:{" + Guid.NewGuid().ToString().ToUpper() + "}");
setupFileContents = expression4.Replace(setupFileContents,"\"ProductVersion\" = \"8:" + productVersion);
using (TextWriter tw = new StreamWriter(setupFileName, false))
{
tw.WriteLine(setupFileContents);
}
}
}
]]>
</code>
</script>
<!-- must build the dependency first (code project), before building the MSI deployment project -->
<exec program="Devenv.exe" commandline='"fullPath\solution.sln" /rebuild "Release" /project "${DependencyProject}" /out "fullPath\somelog.log"'/>
<exec program="Devenv.exe" commandline='"fullPath\solution.sln" /rebuild "Release" /project "${DeploymentProjectPath}" /out "fullPath\somelog.log"'/>
</target>
MSBuild cannot build .vdproj projects. You should use Visual Studio (devenv.com) for this.
FYI this is still not supported in .NET 4.0
Just to expand on CRise's post - building a vdproj project using VS2010 devenv through command line doesn't work. Pre-2010 works fine I believe (see link text)
Let's say I wanted to create a static text file which ships with each release. I want the file to be updated with the version number of the release (as specified in AssemblyInfo.cs), but I don't want to have to do this manually.
I was hoping I could use a post-build event and feed the version number to a batch file like this:
call foo.bat $(AssemblyVersion)
However I can't find any suitable variable or macro to use.
Is there a way to achieve this that I've missed?
If (1) you don't want to download or create a custom executable that retrieves the assembly version and (2) you don't mind editing the Visual Studio project file, then there is a simple solution that allows you to use a macro which looks like this:
#(Targets->'%(Version)')
#(VersionNumber)
To accomplish this, unload your project. If the project somewhere defines a <PostBuildEvent> property, cut it from the project and save it elsewhere temporarily (notepad?). Then at the very end of the project, just before the end-tag, place this:
<Target Name="PostBuildMacros">
<GetAssemblyIdentity AssemblyFiles="$(TargetPath)">
<Output TaskParameter="Assemblies" ItemName="Targets" />
</GetAssemblyIdentity>
<ItemGroup>
<VersionNumber Include="#(Targets->'%(Version)')"/>
</ItemGroup>
</Target>
<PropertyGroup>
<PostBuildEventDependsOn>
$(PostBuildEventDependsOn);
PostBuildMacros;
</PostBuildEventDependsOn>
<PostBuildEvent>echo HELLO, THE ASSEMBLY VERSION IS: #(VersionNumber)</PostBuildEvent>
</PropertyGroup>
This snippet has an example <PostBuildEvent> already in it. No worries, you can reset it to your real post-build event after you have re-loaded the project.
Now as promised, the assembly version is available to your post build event with this macro:
#(VersionNumber)
Done!
If you prefer scripting these methods might also work for you:
If you are using the post-build event, you can use the filever.exe tool to grab it out of the already built assembly:
for /F "tokens=4" %%F in ('filever.exe /B /A /D bin\debug\myapp.exe') do (
set VERSION=%%F
)
echo The version is %VERSION%
Get filever.exe from here: http://support.microsoft.com/kb/913111
If you are using the pre-build event, you can take it out of the AssemblyInfo.cs file as follows:
set ASMINFO=Properties\AssemblyInfo.cs
FINDSTR /C:"[assembly: AssemblyVersion(" %ASMINFO% | sed.exe "s/\[assembly: AssemblyVersion(\"/SET CURRENT_VERSION=/g;s/\")\]//g;s/\.\*//g" >SetCurrVer.cmd
CALL SetCurrVer.cmd
DEL SetCurrVer.cmd
echo Current version is %CURRENT_VERSION%
This uses the unix command line tool sed, which you can download from many places, such as here: http://unxutils.sourceforge.net/ - iirc that one works ok.
This answer is a minor modification of the answer of Brent Arias. His PostBuildMacro worked quite well for me until a version update of Nuget.exe.
In the recent releases, Nuget trims non significant parts of the package version number in order to obtain a semantic version like "1.2.3". For example, the assembly version "1.2.3.0" is formatted by Nuget.exe "1.2.3". And "1.2.3.1" is formatted "1.2.3.1" as expected.
As I need to infer the exact package filename generated by Nuget.exe, I use now this adaptated macro (tested in VS2015):
<Target Name="PostBuildMacros">
<GetAssemblyIdentity AssemblyFiles="$(TargetPath)">
<Output TaskParameter="Assemblies" ItemName="Targets" />
</GetAssemblyIdentity>
<ItemGroup>
<VersionNumber Include="$([System.Text.RegularExpressions.Regex]::Replace("%(Targets.Version)", "^(.+?)(\.0+)$", "$1"))" />
</ItemGroup>
</Target>
<PropertyGroup>
<PostBuildEventDependsOn>
$(PostBuildEventDependsOn);
PostBuildMacros;
</PostBuildEventDependsOn>
<PostBuildEvent>echo HELLO, THE ASSEMBLY VERSION IS: #(VersionNumber)</PostBuildEvent>
</PropertyGroup>
UPDATE 2017-05-24: I corrected the regex in this way: "1.2.0.0" will be translated to "1.2.0" and not "1.2" as previously coded.
And to answer to a comment of Ehryk Apr, you can adapt the regex to keep only some part of the version number. As an example to keep "Major.Minor", replace:
<VersionNumber Include="$([System.Text.RegularExpressions.Regex]::Replace("%(Targets.Version)", "^(.+?)(\.0+)$", "$1"))" />
By
<VersionNumber Include="$([System.Text.RegularExpressions.Regex]::Replace("%(Targets.Version)", "^([^\.]+)\.([^\.]+)(.*)$", "$1.$2"))" />
As a workaround I've written a managed console application which takes the target as a parameter, and returns the version number.
I'm still interested to hear a simpler solution - but I'm posting this in case anyone else finds it useful.
using System;
using System.IO;
using System.Diagnostics;
using System.Reflection;
namespace Version
{
class GetVersion
{
static void Main(string[] args)
{
if (args.Length == 0 || args.Length > 1) { ShowUsage(); return; }
string target = args[0];
string path = Path.IsPathRooted(target)
? target
: Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName) + Path.DirectorySeparatorChar + target;
Console.Write( Assembly.LoadFile(path).GetName().Version.ToString(2) );
}
static void ShowUsage()
{
Console.WriteLine("Usage: version.exe <target>");
}
}
}
I think the best thing you can do is look at MSBuild and MsBuild Extension Pack you should be able to edit you solution file so that a post build event occurs and writes to your test file.
If this is too complicated then you could simply create a small program that inspects all assemblies in you output directory and execute it on post build, you could pass in the output directory using the variable name... for example in the post build event...
AssemblyInspector.exe "$(TargetPath)"
class Program
{
static void Main(string[] args)
{
var assemblyFilename = args.FirstOrDefault();
if(assemblyFilename != null && File.Exists(assemblyFilename))
{
try
{
var assembly = Assembly.ReflectionOnlyLoadFrom(assemblyFilename);
var name = assembly.GetName();
using(var file = File.AppendText("C:\\AssemblyInfo.txt"))
{
file.WriteLine("{0} - {1}", name.FullName, name.Version);
}
}
catch (Exception ex)
{
throw;
}
}
}
}
You could also pass in the text file location...
I've started adding a separate project that builds last and adding a post build event to that project that runs itself. Then I just perform my post build steps programmatically in there.
It makes it a lot easier to do stuff like this. Then you can just inspect the assembly attributes of whatever assembly you want. So far it's working pretty awesome.
From that what I understand...
You need a generator for post build events.
1. Step: Writing a Generator
/*
* Author: Amen RA
* # Timestamp: 2013.01.24_02:08:03-UTC-ANKH
* Licence: General Public License
*/
using System;
using System.IO;
namespace AppCast
{
class Program
{
public static void Main(string[] args)
{
// We are using two parameters.
// The first one is the path of a build exe, i.e.: C:\pathto\nin\release\myapp.exe
string exePath = args[0];
// The second one is for a file we are going to generate with that information
string castPath = args[1];
// Now we use the methods below
WriteAppCastFile(castPath, VersionInfo(exePath));
}
public static string VersionInfo(string filePath)
{
System.Diagnostics.FileVersionInfo myFileVersionInfo = System.Diagnostics.FileVersionInfo.GetVersionInfo(filePath);
return myFileVersionInfo.FileVersion;
}
public static void WriteAppCastFile(string castPath, string exeVersion)
{
TextWriter tw = new StreamWriter(castPath);
tw.WriteLine(#"<?xml version=""1.0"" encoding=""utf-8""?>");
tw.WriteLine(#"<item>");
tw.WriteLine(#"<title>MyApp - New version! Release " + exeVersion + " is available.</title>");
tw.WriteLine(#"<version>" + exeVersion + "</version>");
tw.WriteLine(#"<url>http://www.example.com/pathto/updates/MyApp.exe</url>");
tw.WriteLine(#"<changelog>http://www.example.com/pathto/updates/MyApp_release_notes.html</changelog>");
tw.WriteLine(#"</item>");
tw.Close();
}
}
}
2. Step: Using it as a post build command in our IDE
After the application is running satisfyingly for you:
In your development IDE, use the following command line for post build events.
C:\Projects\pathto\bin\Release\AppCast.exe "C:\Projects\pathto\bin\Release\MyApp.exe" "c:\pathto\www.example.com\root\pathto\updates\AppCast.xml"
I don't know Why but Brent Arias macro not worked for me (#(VersionNumber) always was empty) :( .Net6 VS2022. I ended up with slightly modified version:
<Target Name="GetVersion" AfterTargets="PostBuildEvent">
<GetAssemblyIdentity AssemblyFiles="$(TargetPath)">
<Output TaskParameter="Assemblies" ItemName="AssemblyInfo" />
</GetAssemblyIdentity>
<PropertyGroup>
<VersionInfo>%(AssemblyInfo.Version)</VersionInfo>
</PropertyGroup>
<!--And use it after like any other variable:-->
<Message Text="VersionInfo = $(VersionInfo)" Importance="high" />
</Target>
It should be noted that using the modernized (VS2017+) .csproj formatting and VS2022, $(AssemblyVersion)
as in the original post can now be used directly.
Unless I'm missing something, this is a lot simpler. Put this in your pre or post-build scripts:
FOR /F delims^=^"^ tokens^=2 %%i in ('findstr /b /c:"[assembly: AssemblyVersion("$(ProjectDir)\Properties\AssemblyInfo.cs') do (set version=%%i)
echo Version: %version%
I needed exactly this for automatically putting the number in the readme file in the output folder. In the end, as Winston Smith showed, a small external tool is a very good solution for that, and it has the advantage you can format it however you want.
This app outputs the formatted version to the console. I used it in my post-build events to build the readme file by calling it with >> to redirect its output to the readme file.
public class GetVerNum
{
static void Main(String[] args)
{
if (args.Length == 0)
return;
try
{
FileVersionInfo ver = FileVersionInfo.GetVersionInfo(args[0]);
String version = "v" + ver.FileMajorPart.ToString() + "." + ver.FileMinorPart;
if (ver.FileBuildPart > 0 || ver.FilePrivatePart > 0)
version += "." + ver.FileBuildPart;
if (ver.FilePrivatePart > 0)
version += "." + ver.FilePrivatePart;
Console.Write(version);
}
catch { }
}
}
My post-build events:
<nul set /p dummyset=My Application > "$(ProjectDir)\Readme\readme-header.txt"
"$(ProjectDir)\Readme\GetVersionNumber.exe" "$(TargetPath)" >>"$(ProjectDir)\Readme\readme-header.txt"
echo by Nyerguds>>"$(ProjectDir)\Readme\readme-header.txt"
echo Build date: %date% %time% >> "$(ProjectDir)\Readme\readme-header.txt"
echo.>>"$(ProjectDir)\Readme\readme-header.txt"
copy /b "$(ProjectDir)\Readme\readme-header.txt" + "$(ProjectDir)\Readme\readme-body.txt" "$(TargetDir)\$(ProjectName).txt"
I put all the readme generating related stuff in the \Readme\ folder of my project; the app containing the above code, and the "readme-body.txt" containing the actual readme stuff.
First line: create the "readme-header.txt" file in the \Readme\ folder of my project, and put the program name inside it. (The <nul set /p dummyset= is a trick I found here: Windows batch: echo without new line). You could also store this string in another text file and just copy that to "readme-header.txt" instead.
Second line: run the version number retrieving app with the freshly-generated exe file as parameter, and add its output to the header file.
Third line: add any other stuff (in this case, credits) to the header file. This also adds a line break to the end.
These three together give you a "readme-header.txt" file with "My Application v1.2.3 by Nyerguds", followed by a line break, in it. Then I add the build date and another open line, and copy the header file and the readme body file together to one file in the final build folder. Note that I specifically use binary copy, otherwise it gives odd results. You do have to make sure the body file contains no UTF-8 byte order mark at the start, or you get weird bytes in your final file.
If you have a library project you can try to use WMIC utility (available in windows).
Here is an example. Good thing - you don't need to use any external tools.
SET pathFile=$(TargetPath.Replace("\", "\\"))
FOR /F "delims== tokens=2" %%x IN ('WMIC DATAFILE WHERE "name='%pathFile%'" get Version /format:Textvaluelist') DO (SET dllVersion=%%x)
echo Found $(ProjectName) version %dllVersion%
I looked for the same feature and i found the solution on MSDN.
https://social.msdn.microsoft.com/Forums/vstudio/de-DE/e9485c92-98e7-4874-9310-720957fea677/assembly-version-in-post-build-event?forum=msbuild
$(ApplicationVersion) did the Job for me.
Edit:
Okay I just saw the Problem $(ApplicationVersion) is not from AssemblyInfo.cs, its the PublishVersion defined in the project Properties. It still does the job for me in a simple way. So maybe someone needs it too.
Another Solution:
You can call a PowerShell script on PostBuild, here you can read the AssemblyVersion directly from your Assembly. I call the script with the TargetDir as Parameter
PostBuild Command:
PowerShell -ExecutionPolicy Unrestricted $(ProjectDir)\somescript.ps1 -TargetDir $(TargetDir)
PowerShell Script:
param(
[string]$TargetDir
)
$Version = (Get-Command ${TargetDir}Example.exe).FileVersionInfo.FileVersion
This way you will get the Version from the AssemblyInfo.cs