How do you access publish information in a T4 Text Template? - c#

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;
}
}

Related

throws “Process is terminated due to StackOverflowException” when compiling

I've got an error in only one project when compiling. I already check every classes and cannot find recursive in classes.
I can't fix the problem. I think you can check file of Microsoft.CSharp.Core.targets, and may be you can fix the problem. I make project in VS2017, and i can run. Now, I use VS2019 but i can't build the project.
Error:
1>------ Derleme başladı: Proje: Flight.Utility, Yapılandırma: Debug Any CPU ------
1>C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\Roslyn\Microsoft.CSharp.Core.targets(59,5): error :
1>C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\Roslyn\Microsoft.CSharp.Core.targets(59,5): error : Process is terminated due to StackOverflowException.
Someone can help to me ?
Thank you.
Microsoft.CSharp.Core.targets file:
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -->
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="Microsoft.Managed.Core.targets"/>
<Target Name="CoreCompile"
Inputs="$(MSBuildAllProjects);
#(Compile);
#(_CoreCompileResourceInputs);
$(ApplicationIcon);
$(AssemblyOriginatorKeyFile);
#(ReferencePathWithRefAssemblies);
#(CompiledLicenseFile);
#(LinkResource);
#(EmbeddedDocumentation);
$(Win32Resource);
$(Win32Manifest);
#(CustomAdditionalCompileInputs);
$(ResolvedCodeAnalysisRuleSet);
#(AdditionalFiles);
#(EmbeddedFiles);
#(EditorConfigFiles)"
Outputs="#(DocFileItem);
#(IntermediateAssembly);
#(IntermediateRefAssembly);
#(_DebugSymbolsIntermediatePath);
$(NonExistentFile);
#(CustomAdditionalCompileOutputs)"
Returns="#(CscCommandLineArgs)"
DependsOnTargets="$(CoreCompileDependsOn);_BeforeVBCSCoreCompile">
<!-- These two compiler warnings are raised when a reference is bound to a different version
than specified in the assembly reference version number. MSBuild raises the same warning in this case,
so the compiler warning would be redundant. -->
<PropertyGroup Condition="('$(TargetFrameworkVersion)' != 'v1.0') and ('$(TargetFrameworkVersion)' != 'v1.1')">
<NoWarn>$(NoWarn);1701;1702</NoWarn>
</PropertyGroup>
<PropertyGroup>
<!-- To match historical behavior, when inside VS11+ disable the warning from csc.exe indicating that no sources were passed in-->
<NoWarn Condition="'$(BuildingInsideVisualStudio)' == 'true' AND '$(VisualStudioVersion)' != '' AND '$(VisualStudioVersion)' > '10.0'">$(NoWarn);2008</NoWarn>
</PropertyGroup>
<PropertyGroup>
<!-- If the user has specified AppConfigForCompiler, we'll use it. If they have not, but they set UseAppConfigForCompiler,
then we'll use AppConfig -->
<AppConfigForCompiler Condition="'$(AppConfigForCompiler)' == '' AND '$(UseAppConfigForCompiler)' == 'true'">$(AppConfig)</AppConfigForCompiler>
<!-- If we are targeting winmdobj we want to specifically the pdbFile property since we do not want it to collide with the output of winmdexp-->
<PdbFile Condition="'$(PdbFile)' == '' AND '$(OutputType)' == 'winmdobj' AND '$(_DebugSymbolsProduced)' == 'true'">$(IntermediateOutputPath)$(TargetName).compile.pdb</PdbFile>
</PropertyGroup>
<PropertyGroup>
<LangVersion Condition="'$(LangVersion)' == '' AND
(('$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND '$(TargetFrameworkVersion)' == 'v3.0') OR
('$(TargetFrameworkIdentifier)' == '.NETStandard' AND '$(TargetFrameworkVersion)' == 'v2.1'))">preview</LangVersion>
</PropertyGroup>
<!-- Condition is to filter out the _CoreCompileResourceInputs so that it doesn't pass in culture resources to the compiler -->
<Csc Condition="'%(_CoreCompileResourceInputs.WithCulture)' != 'true'"
AdditionalLibPaths="$(AdditionalLibPaths)"
AddModules="#(AddModules)"
AdditionalFiles="#(AdditionalFiles)"
AllowUnsafeBlocks="$(AllowUnsafeBlocks)"
AnalyzerConfigFiles="#(EditorConfigFiles)"
Analyzers="#(Analyzer)"
ApplicationConfiguration="$(AppConfigForCompiler)"
BaseAddress="$(BaseAddress)"
CheckForOverflowUnderflow="$(CheckForOverflowUnderflow)"
ChecksumAlgorithm="$(ChecksumAlgorithm)"
CodeAnalysisRuleSet="$(ResolvedCodeAnalysisRuleSet)"
CodePage="$(CodePage)"
DebugType="$(DebugType)"
DefineConstants="$(DefineConstants)"
DelaySign="$(DelaySign)"
DisabledWarnings="$(NoWarn)"
DisableSdkPath="$(DisableSdkPath)"
DocumentationFile="#(DocFileItem)"
EmbedAllSources="$(EmbedAllSources)"
EmbeddedFiles="#(EmbeddedFiles)"
EmitDebugInformation="$(DebugSymbols)"
EnvironmentVariables="$(CscEnvironment)"
ErrorEndLocation="$(ErrorEndLocation)"
ErrorLog="$(ErrorLog)"
ErrorReport="$(ErrorReport)"
Features="$(Features)"
FileAlignment="$(FileAlignment)"
GenerateFullPaths="$(GenerateFullPaths)"
HighEntropyVA="$(HighEntropyVA)"
Instrument="$(Instrument)"
KeyContainer="$(KeyContainerName)"
KeyFile="$(KeyOriginatorFile)"
LangVersion="$(LangVersion)"
LinkResources="#(LinkResource)"
MainEntryPoint="$(StartupObject)"
ModuleAssemblyName="$(ModuleAssemblyName)"
NoConfig="true"
NoLogo="$(NoLogo)"
NoStandardLib="$(NoCompilerStandardLib)"
NoWin32Manifest="$(NoWin32Manifest)"
Nullable="$(Nullable)"
Optimize="$(Optimize)"
Deterministic="$(Deterministic)"
PublicSign="$(PublicSign)"
OutputAssembly="#(IntermediateAssembly)"
OutputRefAssembly="#(IntermediateRefAssembly)"
PdbFile="$(PdbFile)"
Platform="$(PlatformTarget)"
Prefer32Bit="$(Prefer32Bit)"
PreferredUILang="$(PreferredUILang)"
ProvideCommandLineArgs="$(ProvideCommandLineArgs)"
References="#(ReferencePathWithRefAssemblies)"
RefOnly="$(ProduceOnlyReferenceAssembly)"
ReportAnalyzer="$(ReportAnalyzer)"
Resources="#(_CoreCompileResourceInputs);#(CompiledLicenseFile)"
ResponseFiles="$(CompilerResponseFile)"
RuntimeMetadataVersion="$(RuntimeMetadataVersion)"
SharedCompilationId="$(SharedCompilationId)"
SkipCompilerExecution="$(SkipCompilerExecution)"
Sources="#(Compile)"
SubsystemVersion="$(SubsystemVersion)"
TargetType="$(OutputType)"
ToolExe="$(CscToolExe)"
ToolPath="$(CscToolPath)"
TreatWarningsAsErrors="$(TreatWarningsAsErrors)"
UseHostCompilerIfAvailable="$(UseHostCompilerIfAvailable)"
UseSharedCompilation="$(UseSharedCompilation)"
Utf8Output="$(Utf8Output)"
VsSessionGuid="$(VsSessionGuid)"
WarningLevel="$(WarningLevel)"
WarningsAsErrors="$(WarningsAsErrors)"
WarningsNotAsErrors="$(WarningsNotAsErrors)"
Win32Icon="$(ApplicationIcon)"
Win32Manifest="$(Win32Manifest)"
Win32Resource="$(Win32Resource)"
PathMap="$(PathMap)"
SourceLink="$(SourceLink)">
<Output TaskParameter="CommandLineArgs" ItemName="CscCommandLineArgs" />
</Csc>
<ItemGroup>
<_CoreCompileResourceInputs Remove="#(_CoreCompileResourceInputs)" />
</ItemGroup>
<CallTarget Targets="$(TargetsTriggeredByCompilation)" Condition="'$(TargetsTriggeredByCompilation)' != ''" />
</Target>
</Project>
I had the same problem only in VS 2019. In VS 2017 everything worked fine. In my case the problem was occurred because of my Fluent API, the call chain was too long (about 400 lines of codes). When I split the call chain into small parts it started working.
I don't know why this happened, probably some problem with the code parser in VS 2019.

Retrieve assembly version for use in Jenkins build

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%

MSBuild custom task depending on another project

I have an odd solution where I need one of the projects to "compile" files in another one.
The compiler (showing here a minimal example) is as follows (MSBuild custom task):
public class MyCompileTask : Task
{
[Required]
public ITaskItem[] InputFiles { get; set; }
[Output]
public ITaskItem[] OutputFiles { get; set; }
public override bool Execute()
{
var generatedFileNames = new List<string>();
foreach (var inputFile in this.InputFiles)
{
var inputFileName = inputFile.ItemSpec;
var outputFileName = Path.ChangeExtension(inputFileName, ".res.txt");
var source = File.ReadAllText(inputFileName);
var compiled = source.ToUpper();
File.WriteAllText(outputFileName, compiled + "\n\n" + DateTime.Now);
generatedFileNames.Add(outputFileName);
}
this.OutputFiles = generatedFileNames.Select(name => new TaskItem(name)).ToArray();
return true;
}
}
As you see, it only uppercases the content of the input files.
This was project A - the "compiler" library.
Project B, for now the main application, has a file "lorem.txt" that needs to be "compiled" into "lorem.res.txt" and put as an EmbeddedResource in B.exe/B.dll.
In B.csproj I added the following:
<PropertyGroup>
<CoreCompileDependsOn>$(CoreCompileDependsOn);InvokeMyCompile</CoreCompileDependsOn>
</PropertyGroup>
<UsingTask TaskName="MyCompiler.MyCompileTask" AssemblyFile="$(MSBuildProjectDirectory)\..\MyCompiler\bin\$(Configuration)\MyCompiler.dll" />
<Target Name="MyCompile" Inputs="lorem.txt" Outputs="lorem.res.txt">
<MyCompileTask InputFiles="lorem.txt">
<Output TaskParameter="OutputFiles" PropertyName="OutputFiles" />
</MyCompileTask>
</Target>
<Target Name="InvokeMyCompile" Inputs="lorem.txt" Outputs="lorem.res.txt">
<Exec Command=""$(MSBuildBinPath)\MSBuild.exe" /t:MyCompile "$(ProjectDir)$(ProjectFileName)"" />
</Target>
(The 2 layers of targets and an explicit msbuild.exe invocation is a workaround to another problem. In fact, much of this example is stolen from that Q.)
The most important part works, i.e. when I change lorem.txt and build, lorem.res.txt gets regenerated.
However:
When lorem.res.txt is physically deleted, a build does nothing (says it's up-to-date) until I actually refresh the project in VS. So, MSBuild does not "know" that lorem.res.txt is actually required to build the project.
More importantly, when I change anything in project A, project B recompiles but does not re-run the compilation lorem.txt -> lorem.res.txt. So MSBuild does not "know" that the transformation is dependent on another project.
How can I declare these dependencies in the csproj file?
Bonus question: how to mark the output file (lorem.res.txt) as a generated EmbeddedResource so I don't have to track it in VS but it's still put into the assembly?
•When lorem.res.txt is physically deleted, a build does nothing (says it's up-to-date) until I actually refresh the project in VS. So, MSBuild does not "know" that lorem.res.txt is actually required to build the project.
I create a demo and reproduce your issue on my side, you could use msbuild command line to avoid it.
•More importantly, when I change anything in project A, project B recompiles but does not re-run the compilation lorem.txt -> lorem.res.txt. So MSBuild does not "know" that the transformation is dependent on another project.
Because the custom task reference the DLL file, when change anything in project A, you need to rebuild project to generate newer DLL file.
Bonus question: how to mark the output file (lorem.res.txt) as a generated EmbeddedResource so I don't have to track it in VS but it's still put into the assembly?
You can add custom ItemGroup in BeforeBuild target to achieve it.
<Target Name="BeforeBuild" DependsOnTargets="MyCompile">
<ItemGroup>
<Content Include="lorem.res.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Target>

How to transform files before adding them to an assembly?

I would like to do the following :
(project is a User Control library for WPF)
add a bunch of .FX (shader source code) files to the project as resources (Build action)
transform each to a .PS file (compiled shader) by invoking FXC.EXE utility
use the resulting file in place of the inputted file
I have been looking to write a CustomTool, unfortunately the tool is never seen by Visual Studio as it's mentioned in the article. In the article it is said that sometimes it is not seen but in my case it translates to every time.
I also looked at MSBuild Transforms but I'm not really sure if it would be appropriate for the task.
The goal of this is to include shader files source code and transform them at build time instead of manually building them from command line and dropping them every time to the project.
Do you know how one can achieve this ? Any methods are welcome
EDIT
Answer thanks to #Luaan :
public class CompileEffectTask : Task
{
public string[] Files { get; set; }
public override bool Execute()
{
if (Files != null)
{
foreach (string file in Files)
{
if (file != null)
{
Log.LogMessage(MessageImportance.High, file);
string s = #"C:\Program Files (x86)\Windows Kits\8.1\bin\x86\fxc.exe";
string changeExtension = Path.ChangeExtension(file, "ps");
string arguments = string.Format("/T ps_3_0 /Fo \"{0}\"" + " " + "\"{1}\"", changeExtension,
file);
Log.LogMessage(MessageImportance.High, arguments);
var process = new Process
{
StartInfo = new ProcessStartInfo(s, arguments)
};
process.Start();
process.WaitForExit();
}
}
}
return true;
}
}
And the MSBuild part :
<UsingTask TaskName="CompileEffectTask" AssemblyFile="D:\HLSLCompiler.dll" />
<PropertyGroup>
<BuildDependsOn>
MyCustomTarget1;
$(BuildDependsOn);
</BuildDependsOn>
</PropertyGroup>
<Target Name="MyCustomTarget1">
<Message Text="CompileEffectTask started" Importance="high" />
<Message Text="Compiling FX files ..." Importance="high" />
<CompileEffectTask Files="#(CompileEffectTask)"/>
<Message Text="Adding resulting .PS files as resources ..." Importance="high" />
<ItemGroup>
<Resource Include="**\*.ps" />
</ItemGroup>
</Target>
<Target Name="AfterBuild">
<CreateItem Include="**\*.ps">
<Output TaskParameter="Include" ItemName="DeleteAfterBuild" />
</CreateItem>
<Delete Files="#(DeleteAfterBuild)" />
</Target>
(still needs some cleaning but it works :D)
Custom tools do work, in fact, but they're rather tricky to setup - they're COM extensions to Visual Studio. However, the better solution for your case would be a custom build target or a pre-build event anyway - custom tools (code generators) are better suited for generating code (text) rather than binary files.
So, the pre-build event is the simple one. It's just some script that's run before the project starts building. You can find it in project properties. The simplest way would be to have all your .fx files in one directory, and in the pre-build event, you'd just call fxc.exe on each of them.
Now, build targets are cooler. They allow you to add your own build actions to files, among other things. So you'd just select CompileEffect in Build action of your files, and magic happens.
The target file can be quite simple:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<AvailableItemName Include="CompileEffect"></AvailableItemName>
</ItemGroup>
</Project>
Or you can just put the ItemGroup part inside of your project file directly (otherwise you'd want to include this target file).
Next, you want to set the task as part of your build:
<PropertyGroup>
<BuildDependsOn>
MyCompileTarget;
$(BuildDependsOn);
</BuildDependsOn>
</PropertyGroup>
This basically says "run my build target first, and after that whatever you'd want".
Now, for the building:
<Target Name="MyCompileTarget">
<CompileEffectTask
ProjectDirectory="$(ProjectDir)"
Files="#(CompileEffect)"
RootNamespace="$(RootNamespace)">
</CompileEffectTaskTask>
</Target>
How does Visual Studio know what CompileEffectTask is?
<UsingTask TaskName="MyAssembly.CompileEffectTask"
AssemblyFile="C:\MyAssembly.dll"/>
And then you just need to implement the compiler task itself.
Now, if you only want to call an executable or a batch script, you don't even need that custom task, because there's a lot of built-in tasks in MSBuild (and even more in MSBuild Community Tasks). Exec task should work:
<Target Name="MyCompileTarget">
<Exec Command="fxc.exe #(CompileEffect)" />
</Target>
You might have to write a for cycle there, I'm not entirely sure. There's a lot of things you can do to customize project builds, http://msdn.microsoft.com/en-us/library/0k6kkbsd.aspx (especially the Task refecence part) is a rather good start.

Embed git commit hash in a .NET dll

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).

Categories