Unable To Set Application Property Using MSbuild.exe - visual-studio

If I go to Project->MyApp->Properties->Settings and enter Name like WhichApp of type string and enter the Scope as Application and Value of PCB. When I show the value in
my main form using MessageBox.Show(Properties.Settings.Default.WhichApp); I see a message box showing "PCB".
But if I try to set the property using MSBuild.exe -property:WhichApp=SUB when I view the property value I still see "PCB".
How can I call the MSBuild.exe compiler tool to set the property at build time?
I tried used External Tools in Visual Studio..
Title: Set Property
Command: C:\Program Files(x86)\Microsoft Visual Studio\2017\WDExpress\MSBuild\15.0\Bin\MSBuild.exe
Arguments: -property:WhichApp=SUB
Initial Directory: $(ProjectDir)
---- WHAT I'M TRY TO ACCOMPLISH IN LOAD() METHOD OF MAIN FORM ----
if (Properties.Settings.Default.PCAppConfig == "PCB")
{
// create new PCB form
// PCBForm.ShowDialog();
}
else if (Properties.Settings.Default.PCAppConfig == "SUB")
{
// create new SUB form
// SUBForm.ShowDialog();
}
--- In solution by jtijn, I tried this to add another property called VersionNum but fails... ---
<Target Name="BeforeBuild">
<PropertyGroup>
<!--Default value.-->
<PcAppConfig Condition="'$(PcAppConfig)' == ''">PCB</PcAppConfig>
<VersionNum Condition="'$(VersionNum)' == ''">1.0.0.66</VersionNum>
<!--The source code.-->
<TheSourceCode>internal static class PcAppConfig{ public static readonly string value = "$(PcAppConfig)"%3B%0A }internal static class VersionNum{ public static readonly string value = "$(VersionNum)"%3B</TheSourceCode>
</PropertyGroup>
<!--Get current source so we can only create it again when needed, to avoid being recompiled.-->
<ReadLinesFromFile File="PcAppConfig.cs" ContinueOnError="True">
<Output TaskParameter="Lines" ItemName="CurrentSourceCode" />
</ReadLinesFromFile>
<!--Write source, if needed.-->
<WriteLinesToFile File="PcAppConfig.cs" Overwrite="True" Lines="$(TheSourceCode)" Condition="'#(CurrentSourceCode)' != '$(TheSourceCode)'" />
</Target>
--- Doesn't compile when values are changed, Fails to create setup files ...
My arguments to MSBuild.exe
SelectApp.csproj.user /p:PcAppConfig=SUB /p:VersionNum=1.0.0.67
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Compile Update="Form1.cs">
<SubType>Form</SubType>
</Compile>
</ItemGroup>
<Target Name="BeforeBuild">
<PropertyGroup>
<!--Default value.-->
<PcAppConfig Condition="'$(PcAppConfig)' == ''">PCB</PcAppConfig>
<VersionNum Condition="'$(VersionNum)' == ''">1.0.0.66</VersionNum>
<!--The source code.-->
<TheSourceCode>internal static class PCAppConfig{ public static readonly string value = "$(PcAppConfig)"%3B }%0Ainternal static class VersionNum{ public static readonly string value = "$(VersionNum)"%3B }</TheSourceCode>
</PropertyGroup>
<!--Get current source so we can only create it again when needed, to avoid being recompiled.-->
<ReadLinesFromFile File="PcAppConfig.cs" ContinueOnError="True">
<Output TaskParameter="Lines" ItemName="CurrentSourceCode" />
</ReadLinesFromFile>
<!--Write source, if needed.-->
<WriteLinesToFile File="PcAppConfig.cs" Overwrite="True" Lines="$(TheSourceCode)" Condition="'#(CurrentSourceCode)' != '$(TheSourceCode)'" />
<ItemGroup>
<Compile Update="PcAppConfig.cs">
<SubType>Component</SubType>
</Compile>
</ItemGroup>
</Target>
<Target Name="AfterCompile">
<Exec Command=""$(ProgramFiles)\Microsoft Visual Studio\2019\Professional\Common7\IDE\devenv.com" C:\DummyApps\SelectApp\PCBSetup\PCBSetup.vdproj /build "Debug|AnyCPU""/>
</Target>
</Project>

You can have the project generate C# source code based on an msbuild property. This target goes at the end in the project file:
<Target Name="BeforeBuild">
<PropertyGroup>
<!--Default value.-->
<PcAppConfig Condition="'$(PcAppConfig)' == ''">PCB</PcAppConfig>
<!--The source code.-->
<TheSourceCode>internal static class PcAppConfig{ public static readonly string value = "$(PcAppConfig)"%3B }</TheSourceCode>
</PropertyGroup>
<!--Get current source so we can only create it again when needed, to avoid being recompiled.-->
<ReadLinesFromFile File="PcAppConfig.cs" ContinueOnError="True">
<Output TaskParameter="Lines" ItemName="CurrentSourceCode" />
</ReadLinesFromFile>
<!--Write source, if needed.-->
<WriteLinesToFile File="PcAppConfig.cs" Overwrite="True" Lines="$(TheSourceCode)" Condition="'#(CurrentSourceCode)' != '$(TheSourceCode)'" />
</Target>
The resulting source file needs to be added to your project, can do this in VS after building once or else add this line along with the other Compile items:
<Compile Include="PcAppConfig.cs" />
Now in your code you can just use PcAppConfig.value.
To change the value build like
msbuild my.csproj /p:PcAppConfig=SUB

Related

Open Telemetry with .Net 7 minimal api - AttachLogsToActivityEvent not working with 'IncludeFormattedMessage'

I am using 'OpenTelemetry.Contrib.Preview' package to attach ILogger standalone logs as Activity Events (aka Trace events) to co-relate the logs with trace & show at a single place like Jaeger. As part of the documentation if we want to attach the logs we need to call 'AttachLogsToActivityEvent' along with setting 'IncludeFormattedMessage' to true during the log setup at startup.cs. While I see the logs are successfully being attached to the activity but only the default 3 properties CategoryName, LogLevel & EventId are being added and the actual 'FormattedMessage' is not getting logged even though I have it enabled. Searched in their github repo where I found this below issue, but the same is not working here: https://github.com/open-telemetry/opentelemetry-dotnet-contrib/issues/134
Any idea what's missing?
csproj file
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0-*" />
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="7.0.0" />
<PackageReference Include="OpenTelemetry.Contrib.Preview" Version="1.0.0-beta2" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.4.0-rc.1" />
<PackageReference Include="OpenTelemetry.Exporter.Jaeger" Version="1.4.0-rc.1" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.4.0-rc.1" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.HttpListener" Version="1.4.0-rc.1" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.4.0-rc.1" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs" Version="1.4.0-rc.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.4.0-rc.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.0.0-rc9.10" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.0.0-rc9.10" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.1.0-beta.2" />
</ItemGroup>
</Project>
Program.cs file (there are bunch of lines for swagger/versioning testing)
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OpenTelemetry;
using OpenTelemetry.Exporter;
using OpenTelemetry.Instrumentation.AspNetCore;
using OpenTelemetry.Logs;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System;
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
services.AddProblemDetails();
services.AddEndpointsApiExplorer();
services.AddApiVersioning(options => options.ReportApiVersions = true)
.AddApiExplorer(
options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
})
.EnableApiVersionBinding();
services.AddSwaggerGen();
builder.Services.Configure<AspNetCoreInstrumentationOptions>(options => options.RecordException = true);
var resource = ResourceBuilder.CreateDefault().AddService("MyService");
builder.Services
.AddOpenTelemetry()
.ConfigureResource(ConfigureResource)
.WithTracing(o =>
{
o.SetSampler(new TraceIdRatioBasedSampler(1.0))
.AddSource("MyService")
.AddAspNetCoreInstrumentation(option => option.RecordException = true)
.AddHttpClientInstrumentation();
o.AddConsoleExporter(o => o.Targets = ConsoleExporterOutputTargets.Console);
}).StartWithHost();
builder.Logging.ClearProviders();
builder.Logging.Configure(o => o.ActivityTrackingOptions = ActivityTrackingOptions.SpanId | ActivityTrackingOptions.TraceId | ActivityTrackingOptions.ParentId);
builder.Logging.AddFilter<OpenTelemetryLoggerProvider>("*", LogLevel.Information);
builder.Logging
.AddOpenTelemetry(loggerOptions =>
{
loggerOptions.SetResourceBuilder(resource);
loggerOptions.IncludeFormattedMessage = true;
loggerOptions.IncludeScopes = true;
loggerOptions.ParseStateValues = true;
loggerOptions.AddConsoleExporter();
loggerOptions.AttachLogsToActivityEvent();
});
var app = builder.Build();
var common = app.NewVersionedApi("Common");
var commonV1 = common.MapGroup("/api/v{version:apiVersion}/common").HasApiVersion(1.0);
commonV1.MapGet("/", (ILogger<Program> logger) =>
{
logger.LogInformation("Calling hello world api!");
return Results.Ok("Hello World common v1!");
});
app.UseSwagger();
app.UseSwaggerUI(
options =>
{
var descriptions = app.DescribeApiVersions();
foreach (var description in descriptions)
{
var url = $"/swagger/{description.GroupName}/swagger.json";
var name = description.GroupName.ToUpperInvariant();
options.SwaggerEndpoint(url, name);
}
});
app.Run();
static void ConfigureResource(ResourceBuilder r) => r.AddService(
serviceName: "MyService",
serviceVersion: "1.0.0",
serviceInstanceId: Environment.MachineName);
Once I run the above program & execute the endpoint, I see the activity is getting logged with the additional 'log' even that I added as part of the endpoint execution, but it doesn't include the actual 'FromattedMessage' property though, like an example below...
I have opened the same issue in github too: https://github.com/open-telemetry/opentelemetry-dotnet/issues/4052

How to Publish to Folder while including Assembly Version in Path in Visual Studio 2022?

I'm building a .Net 6 WPF application in Visual Studio 2022 and I'm publishing to a folder.
I'd like the assembly version included in the folder path e.g. C:\Code\Publish\MyApplication_1.2.0.28\
My Google Fu is failing me and I can't find a solution.
I'm using a Text Template to generate the Assembly Version from a manually set <major>.<minor>.<patch>. and an autoincrementing <revision>. See below. This works exceptionally well. Thanks to https://makolyte.com/auto-increment-build-numbers-in-visual-studio/ including comments.
Following this StackOverflow I tried creating and calling a static method getting the assembly version from the publishprofile. I added the method to the .tt just to keep it together. The static method works if I just call it from MainWindow, but when editing the FolderProfile.pubxml as text and adding the call to the PublishDir: <PublishDir>C:\Code\Publish\MyApplication\$([VersionGetter]::GetVersion())</PublishDir>
and hitting the Publish-button, it opens the wizard to create a new publishprofile, so that is not correct syntax. From the linked documentation, it looks like I can call certain types from here. It works creating a GUID string with the following, so I guess that's just not the way to go: <PublishDir>C:\Code\Publish\MyApplication\$([System.Guid]::NewGuid())</PublishDir>
I actually stumpled upon this StackOverflow answer, but I'm unsure of how to implement it. It creates a directory, but how does the publish profile know about this directory? See code from that answer at the buttom.
So, StackOverflow, please help me :)
FolderProfile.pubxml
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>C:\Code\Publish\MyApplication\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net6.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<PublishReadyToRun>false</PublishReadyToRun>
</PropertyGroup>
</Project>
Added this to .csproj file
<PropertyGroup>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TextTemplating\Microsoft.TextTemplating.targets" />
<!-- Automatic versioning end -->
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DebugType>embedded</DebugType>
<TransformOnBuild>true</TransformOnBuild>
<OverwriteReadOnlyOutputFiles>true</OverwriteReadOnlyOutputFiles>
<TransformOutOfDateOnly>false</TransformOutOfDateOnly>
</PropertyGroup>
Text Template: VersionAutoIncrement.tt
<## template debug="false" hostspecific="true" language="C#" #>
<## output extension=".cs" #>
<## assembly name="System.Core" #>
<## import namespace="System.IO" #>
<## import namespace="System.Linq" #>
<#
try
{
string currentDirectory = Path.GetDirectoryName(this.Host.TemplateFile);
string fullPath = Path.Combine(currentDirectory, "VersionAutoIncrement.cs");
string currentRevisionNumber = File.ReadLines(fullPath).First().Replace("//", "");
Revision = Convert.ToUInt16(currentRevisionNumber);
Revision++;
}
catch( Exception )
{
//Throws the first time since the output file doesn’t exist yet
}
#>
//<#= this.Revision #>
using System.Reflection;
[assembly: AssemblyVersion("1.2.0.<#= this.Revision #>")]
namespace FlexPOS_Importer
{
public static class VersionGetter
{
public static string GetVersion()
{
return Assembly.GetExecutingAssembly().GetName().Version.ToString();
}
}
}
<#+
UInt16 Revision = 1;
#>
The generated VersionAutoIncrement.cs file
//29
using System.Reflection;
[assembly: AssemblyVersion("1.2.0.29")]
namespace FlexPOS_Importer
{
public static class VersionGetter
{
public static string GetVersion()
{
return Assembly.GetExecutingAssembly().GetName().Version.ToString();
}
}
}
Section copied from this StackOverflow answer
As follows[Altered version of the code as described in the given
links]:
<PropertyGroup>
<AssemblyList>myfolder\myLibrary.dll</AssemblyList>
</PropertyGroup>
<Target Name="AssemblyInformations">
<GetAssemblyIdentity AssemblyFiles="$(AssemblyList)">
<Output TaskParameter="Assemblies" ItemName="AssemblyInfos"/>
</GetAssemblyIdentity>
</Target>
<Message Text="Files: %(AssemblyInfos.Version)"/>
And then I would use something like the following to create a directory in the place you
want to publish:
<CreateProperty Value="$(Public_Shared_Folder)$(ProjectName)\">
<Output TaskParameter="Value" PropertyName="PublicFolderToDropZip" />
</CreateProperty>
<MakeDir Directories="$(PublicFolderToDropZip)"
Condition="$(Configuration)=='Release' AND Exists('$(PublicFolderToDropZip)')" />
The entire operation can be completely automated.

automatic set of assembly version in csproj via task

I want to set Assembly Version of C# project in csproj file with a self generated task.
I want to set Assembly Version to:
Major Version -> Project version: for example 3
Minor Version -> 0
Build Number -> Year + Calendarweek
Revision -> Current day of week
My idea was the following code.
But currently it is not working.
Any ideas what I should change?
<UsingTask TaskName="DetectAssemblyVersion" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
<ParameterGroup>
<Result ParameterType="System.String" Output="true" />
</ParameterGroup>
<Task>
<Code Type="Fragment" Language="cs">
<![CDATA[
string year = System.DateTime.Now.ToString("yy");
int dayOfWeek = (int) System.DateTime.Now.DayOfWeek;
System.Globalization.DateTimeFormatInfo dfi = System.Globalization.DateTimeFormatInfo.CurrentInfo;
System.Globalization.Calendar cal = dfi.Calendar;
string calendarWeek = cal.GetWeekOfYear(System.DateTime.Now, dfi.CalendarWeekRule,
dfi.FirstDayOfWeek).ToString();
Result = "3.0." + year + calendarWeek + "."+ dayOfWeek.ToString();
Log.LogMessage(MessageImportance.High, "detect of current version: " + Result);
]]>
</Code>
</Task>
</UsingTask>
<Target Name="TestBuild" AfterTargets="Build">
<DetectAssemblyVersion>
<Output PropertyName="AssemblyVersion" TaskParameter="Result" />
</DetectAssemblyVersion>
<Message Importance="high" Text="new version: $(AssemblyVersion)" />
</Target>

What is syntax of nested property of MSBuild?

I am using MSBuild.
I am getting the value of the Person_1 through the $(Person_1). How can I get the value of the Name subelement of Person_2? I need the syntax.
<PropertyGroup>
<Person_1>Bob</Person_1>
<Person_2>
<Name>Bob</Name>
</Person_2>
</PropertyGroup>
RE: https://msdn.microsoft.com/en-us/library/ms171458.aspx
A property that contains XML is simply that. You cannot access parts of the content just because it is XML. To understand this do the following;
<PropertyGroup>
<MyProperty>
<PropertyContentXML>
<InnerXML1>Blablabla</InnerXML1>
<InnerXML2>More blablabla</InnerXML2>
</PropertyContentXML>
</MyProperty>
</PropertyGroup>
<Target Name="Build">
<Message Text="$(MyProperty)" />
</Target>
The output of this will be:
<PropertyContentXML xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<InnerXML1>Blablabla</InnerXML1>
<InnerXML2>More blablabla</InnerXML2>
</PropertyContentXML>
You are mixing Properties and ItemGroups.
Properties are simple named values, ItemGroups are items with an identity and with properties. You can not use both in the same way.
Properties are defined as :
<PropertyGroup>
<name>value</name>
</Propertygroup>
and are accessed by using the $(name) syntax.
Item groups are defined as:
<ItemGroup>
<Item Include="item1">
<ItemPropery>value</ItemProperty>
</Item>
</ItemGroup>
and are accessed by using this syntax: %(Item.ItemProperty).
See also this reference for the 'intuitive' syntax
You'll need something advanced, like an inline task:
<UsingTask TaskName="TransformXmlToItem"
TaskFactory="CodeTaskFactory"
AssemblyName="Microsoft.Build.Tasks.Core">
<ParameterGroup>
<Xml Required="true"/>
<Elements ParameterType="Microsoft.Build.Framework.ITaskItem[]" Output="true"/>
</ParameterGroup>
<Task>
<Reference Include="System.Xml" />
<Using Namespace="System.Collections.Generic" />
<Using Namespace="System.Xml" />
<Code Type="Fragment" Language="cs">
<![CDATA[
using (var xr = new XmlTextReader(Xml, XmlNodeType.Element,
new XmlParserContext(null, null, null, XmlSpace.Default))) {
xr.Namespaces = false;
xr.MoveToContent();
var items = new List<ITaskItem>();
while (!xr.EOF) {
if (xr.NodeType == XmlNodeType.Element) {
var item = new TaskItem(xr.Name);
var text = xr.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(text)) {
item.SetMetadata("text", text);
}
}
xr.Read();
}
Elements = items.ToArray();
}
]]>
</Code>
</Task>
The task reads the XML elements and creates items from it. The text is transformed into metadata.
You can then write a task like this:
<Target Name="DeconstructPropertyXml">
<TransformXmlToItem Xml="$(Person_2)">
<Output TaskParameter="Elements" ItemName="Person_2I"/>
</TransformXmlToItem>
<Message Text="%(Person_2I.Identity) = %(Person_2I.text)" Importance="high"/>
</Target>
Which should just output Name = Bob.
The same way you could add additional metadata from attributes, etc.

Getting the file version of a native exe in MSBuild

I have a number of Visual C++ projects in a Visual Studio 2010 solution. Also in this solution is a WiX project that builds the installer for the executable that is the product of one of the C++ projects.
The executable has a resource file in its project which writes the version of the program to the executable.
Now I'd like to version the WiX-built installer with the same number as the one written to the executable by the resource file. I've searched the WiX related posts on StackOverflow and found this post:
Referencing a WixVariable defined in a WiX Library Project from a WiX Setup Project
The accepted answer to which, seems to indicate that a possible solution is to use MSBuild and the GetAssemblyIdentity task in the BeforeBuild Target to acquire the version number from another file (in the case of the SO question a DLL, in my case the executable) and expose it to WiX before WiX builds the installer.
I tried adding this to the MSBuild portion of my .wixproj file but when I try to build the installer I get an error returned saying:
error MSB3441: Cannot get assembly name for "<ExePath>". Could not load file or assembly '<ExeName>.exe' or one of its dependencies. The module was expected to contain an assembly manifest.
I can't seem to find any information on MSDN about this error as it relates to MSBuild. I've checked the built executable and it definitely has a version number on it (as well as the rest of the information from the .rc file) and the WiX project depends on the project that outputs the executable; so I assume its BeforeBuild task is running after the project it depends on has been built completely.
Should I be using a different task instead of GetAssemblyIdentity to retrieve the version number from the .exe in MSBuild, are there other requirements to satisfy before GetAssemblyIdentity will work, or is it just not possible to get this type of information about .exe files in MSBuild?
EDIT :
I accepted Rob's answer since I was misunderstanding the difference between ProductVersion and FileVersion, and the WiX technique that he suggested was working as intended and is a step towards the solution I needed.
FileVersion is an attribute of executables only. Msi files are essentially databases and ProductVersion is an entry in that database; they have no FileVersion attribute to set. The method he suggests correctly sets ProductVersion in the .msi database.
The title of this question is now not really related to what the problem I actually had was, since I was asking for a solution I believed I needed at the time. I've now solved the root problem which was simply getting access to the ProductVersion of the installer. I found a cscript script posted online here: http://kentie.net/article/wixnameversion/index.htm that shows how to access the ProductVersion of the .msi. Using that has allowed me to extract the ProductVersion and use it in other tools.
An easier solution, if you don't need the version in MSBuild, is to just reference the version of the file directly in your .wxs file. Here's a snippet that shows what to do:
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Product Version="!(bind.fileVersion.ExeWithVersion)" ...>
...
<Component ...>
<File Id="ExeWithVersion" Source="path\to\your\versioned\file.exe" />
</Component>
...
</Product>
</Wix>
The magic is that the !(bind.fileVersion.Xxx) says to look up the File element with Id='Xxx' and get its version. This is far away the easiest way to get the version of the file into your MSI package.
I needed the file version one time, and I ended up writing a custom task to get the FileVersion because I couldn't find anything.
namespace GranadaCoder.Framework.CrossDomain.MSBuild.Tasks.IO//.FileVersionTask
{
using System;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Globalization;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Security;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
public class FileVersionTask : FileBasedTaskBase
{
private static readonly string ROOT_DIRECTORY = "myrootdir";
private static readonly string FULL_PATH = "myfullpath";
private static readonly string FILE_NAME = "myfilename";
private static readonly string DIRECTORY = "mydirectory";
private static readonly string EXTENSION = "myextension";
private static readonly string VERSION = "myfileversion";
/// <summary>
/// Gets or sets the source files.
/// </summary>
/// <value>The source files.</value>
[Required]
public string SourceFiles { get; set; }
/// <summary>
/// Gets the file versions as a Task Output property.
/// </summary>
/// <value>The file versions.</value>
[Output]
public ITaskItem[] FileVersions
{ get; private set; }
/// <summary>
/// Task Entry Point.
/// </summary>
/// <returns></returns>
protected override bool AbstractExecute()
{
InternalExecute();
return !Log.HasLoggedErrors;
}
/// <summary>
/// Internal Execute Wrapper.
/// </summary>
private void InternalExecute()
{
IList<string> files = null;
if (String.IsNullOrEmpty(this.SourceFiles))
{
Log.LogWarning("No SourceFiles specified");
return;
}
if (!String.IsNullOrEmpty(this.SourceFiles))
{
Console.WriteLine(this.SourceFiles);
files = base.ConvertSourceFileStringToList(this.SourceFiles);
}
//List<string> fileVersions = new List<string>();
ArrayList itemsAsStringArray = new ArrayList();
foreach (string f in files)
{
FileInfoWrapper fiw = null;
fiw = this.DetermineFileVersion(f);
IDictionary currentMetaData = new System.Collections.Hashtable();
currentMetaData.Add(ROOT_DIRECTORY, fiw.RootDirectory);
currentMetaData.Add(FULL_PATH, fiw.FullPath);
currentMetaData.Add(FILE_NAME, fiw.FileName);
currentMetaData.Add(DIRECTORY, fiw.Directory);
currentMetaData.Add(EXTENSION, fiw.Extension);
currentMetaData.Add(VERSION, fiw.Version);
itemsAsStringArray.Add(new TaskItem(fiw.Version, currentMetaData));
}
this.FileVersions = (ITaskItem[])itemsAsStringArray.ToArray(typeof(ITaskItem));
}
/// <summary>
/// Determines the file version.
/// </summary>
/// <param name="fileName">Name of the file.</param>
/// <returns>File version or 0.0.0.0 if value cannot be determined</returns>
private FileInfoWrapper DetermineFileVersion(string fileName)
{
FileInfoWrapper fiw = new FileInfoWrapper();
fiw.Directory = string.Empty;
fiw.Extension = string.Empty;
fiw.FileName = string.Empty;
fiw.FullPath = string.Empty;
fiw.RootDirectory = string.Empty;
fiw.Version = "0.0.0.0";
try
{
if (System.IO.File.Exists(fileName))
{
fiw.Extension = System.IO.Path.GetExtension(fileName);
fiw.FileName = System.IO.Path.GetFileNameWithoutExtension(fileName);
fiw.FullPath = fileName;// System.IO.Path.GetFileName(fileName);
fiw.RootDirectory = System.IO.Path.GetPathRoot(fileName);
//Take the full path and remove the root directory to mimic the DotNet default behavior of '%filename'
fiw.Directory = System.IO.Path.GetDirectoryName(fileName).Remove(0, fiw.RootDirectory.Length);
FileVersionInfo fvi = FileVersionInfo.GetVersionInfo(fileName);
if (null != fvi)
{
if (null != fvi.FileVersion)
{
fiw.Version = fvi.FileVersion;
}
}
}
}
catch (Exception ex)
{
if (ex is IOException
|| ex is UnauthorizedAccessException
|| ex is PathTooLongException
|| ex is DirectoryNotFoundException
|| ex is SecurityException)
{
Log.LogWarning("Error trying to determine file version " + fileName + ". " + ex.Message);
}
else
{
Log.LogErrorFromException(ex);
throw;
}
}
return fiw;
}
/// <summary>
/// Internal wrapper class to hold file properties of interest.
/// </summary>
internal sealed class FileInfoWrapper
{
public string Directory { get; set; }
public string Extension { get; set; }
public string FileName { get; set; }
public string FullPath { get; set; }
public string RootDirectory { get; set; }
public string Version { get; set; }
}
}
}
.msbuild example
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="AllTargetsWrapper" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<UsingTask AssemblyFile="GranadaCoder.Framework.CrossDomain.MSBuild.dll" TaskName="FileVersionTask"/>
<Target Name="AllTargetsWrapper">
<CallTarget Targets="FileVersionTask1" />
<CallTarget Targets="FileVersionTask2" />
</Target>
<PropertyGroup>
<WorkingCheckout>c:\Program Files\MSBuild</WorkingCheckout>
</PropertyGroup>
<ItemGroup>
<MyTask1ExcludeFiles Include="$(WorkingCheckout)\**\*.rtf" />
<MyTask1ExcludeFiles Include="$(WorkingCheckout)\**\*.doc" />
</ItemGroup>
<ItemGroup>
<MyTask1IncludeFiles Include="$(WorkingCheckout)\**\*.exe" Exclude="#(MyTask1ExcludeFiles)" />
</ItemGroup>
<Target Name="FileVersionTask1">
<FileVersionTask SourceFiles="#(MyTask1IncludeFiles)" >
<Output TaskParameter="FileVersions" ItemName="MyFileVersionItemNames"/>
</FileVersionTask>
<Message Text=" MyFileVersionItemNames MetaData "/>
<Message Text=" ------------------------------- "/>
<Message Text=" "/>
<Message Text="directory: "/>
<Message Text="#(MyFileVersionItemNames->'%(mydirectory)')"/>
<Message Text=" "/>
<Message Text=" "/>
<Message Text="extension: "/>
<Message Text="#(MyFileVersionItemNames->'%(myextension)')"/>
<Message Text=" "/>
<Message Text=" "/>
<Message Text="filename: "/>
<Message Text="#(MyFileVersionItemNames->'%(myfilename)')"/>
<Message Text=" "/>
<Message Text=" "/>
<Message Text="fullpath: "/>
<Message Text="#(MyFileVersionItemNames->'%(myfullpath)')"/>
<Message Text=" "/>
<Message Text=" "/>
<Message Text="rootdir: "/>
<Message Text="#(MyFileVersionItemNames->'%(myrootdir)')"/>
<Message Text=" "/>
<Message Text=" "/>
<Message Text="fileversion: "/>
<Message Text="#(MyFileVersionItemNames->'%(myfileversion)')"/>
<Message Text=" "/>
<Message Text=" "/>
<Message Text=" "/>
<Message Text=" "/>
<Message Text="rootdir + directory + filename + extension: "/>
<Message Text="#(MyFileVersionItemNames->'%(myrootdir)%(mydirectory)%(myfilename)%(myextension)')"/>
<Message Text=" "/>
<Message Text=" "/>
<Message Text="List of files using special characters (carriage return)"/>
<Message Text="#(MyFileVersionItemNames->'"%(myfullpath)"' , '%0D%0A')"/>
<Message Text=" "/>
<Message Text=" "/>
</Target>
<ItemGroup>
<MyTask2IncludeFiles Include="c:\windows\notepad.exe" />
</ItemGroup>
<Target Name="FileVersionTask2">
<FileVersionTask SourceFiles="#(MyTask2IncludeFiles)" >
<Output TaskParameter="FileVersions" PropertyName="SingleFileFileVersion"/>
</FileVersionTask>
<Message Text="SingleFileFileVersion = $(SingleFileFileVersion) "/>
</Target>
</Project>

Resources