MSBuild: Ensure a target is run before any other build steps - c#

I am trying to have the AssemblyInfo.cs file updated to reflect the next Publish version of the project BEFORE any other build steps occur.
in my project file i added before the end:
<Import Project="$(ProjectDir)Properties\PublishVersion.proj" />
PublishVersion.proj looks like this:
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>
<Target Name="BeforeBuild">
<FormatVersion Version="$(ApplicationVersion)" Revision="$(ApplicationRevision)">
<Output TaskParameter="OutputVersion" PropertyName="SetVersion" />
</FormatVersion>
<FileUpdate Files="$(ProjectDir)Properties\AssemblyInfo.cs"
Regex="\d+\.\d+\.\d+\.\d+"
ReplacementText="$(SetVersion)" />
</Target>
</Project>
Now it does execute somewhere before building completes but definitely not before it starts, because when i look at the generated exe it has a file version that was in AssemblyInfo.cs BEFORE build started but the .application file and manifest file have various references to the new version.
resulting manifest file(1.0.0.0 before build start, 1.0.0.4 after build):
<?xml version="1.0" encoding="utf-8"?>
<asmv1:assembly ...>
<asmv1:assemblyIdentity name="BHTechSupport.exe" version="1.0.0.4" ... />
...
<entryPoint>
<assemblyIdentity name="BHTechSupport" version="1.0.0.0" ... />
...
</entryPoint>
...
<dependency>
<dependentAssembly ... codebase="BHTechSupport.exe" ...>
<assemblyIdentity name="BHTechSupport" version="1.0.0.0" ... />
...
</dependentAssembly>
</dependency>
...
</asmv1:assembly>
so how do i ensure that my target gets executed before EVERYTHING else?
making changes to PublishVersion.proj sometimes seem to not take effect and i need to clean the solution and restart visual studio before effecting.

Maybe you have to use BeforeBuild target.
so how do i ensure that my target gets executed before EVERYTHING
else?
To check your target execution, you can change the MSBuild project build output verbosity to Diagnostic. Find it under the Tools > Options > Projects and Solutions > Build and Run. Then build the project and find your target in the output window.
making changes to PublishVersion.proj sometimes seem to not take
effect and i need to clean the solution and restart visual studio
before effecting.
AFAIK, the Visual Studio loads target files once, so you have to reload your project to see the result.
Update I don't know your versioning system, whereas it's not important. Anyway, I tried to provide a simple example for you.
using System;
using System.IO;
using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;
namespace Foo.Build.Tasks {
public sealed class GenerateVersion : Task {
[Output]
public ITaskItem[] GeneratedFiles { get; private set; }
public override bool Execute() {
string objPath = Path.Combine(Path.GetDirectoryName(this.BuildEngine3.ProjectFileOfTaskNode), "obj");
string path = Path.Combine(objPath, "VersionInfo.cs");
File.WriteAllText(path, #"using System.Reflection;
[assembly: AssemblyVersion(""2.0.0"")]");
GeneratedFiles = new[] { new TaskItem(path) };
return true;
}
}
}
The preceding task generates a file that contains AssemblyVersion metadata or everything you want.
At last, you have to change your project file as follows:
<UsingTask TaskName="Foo.Build.Tasks.GenerateVersion" AssemblyFile="Foo.Build.Tasks.dll" />
<Target Name="BeforeBuild">
<GenerateVersion>
<Output TaskParameter="GeneratedFiles" ItemName="Compile" />
</GenerateVersion>
</Target>

If you want a target to run at the beginning of every build regardless of what end-goal target is called (Clean, Restore, Build, Publish) you can use the InitialTargets attribute of the Project element.

Related

Using Msbuild variables in Project Extensions

I currently have a solution with a web.api project that I want to deploy to different virtual directories in my local IIS. Currently I am doing the following in the .csproj of the api:
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(Configuration)' == 'CustomerOne.Debug'">
<CustomerName>CustomerOne</CustomerName>
....
</PropertyGroup>
...
These variables are used extenisvely further on for web.config transforms, copying to different locations, etc., by referencing them like $(CustomerName).
The only place where it does not work is in the definition of the virtual directory, i.e., I'd like to connect the build configuration to the IISUrl below, which you can hardcode:
<ProjectExtensions>
<VisualStudio>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
<WebProjectProperties>
...
<IISUrl>http://localhost/api/something</IISUrl>
...
</WebProjectProperties>
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
Replacing this by <IISUrl>http://localhost/api/$(CustomerName)</IISUrl> does not work. Ideas?
Replacing this by http://localhost/api/$(CustomerName) does not work. Ideas?
That because Anything inside of a ProjectExtensions element will be ignored by MSBuild.
You can get the detailed info from this document ProjectExtensions Element (MSBuild):
Allows MSBuild project files to contain non-MSBuild information.
Anything inside of a ProjectExtensions element will be ignored by
MSBuild.
That is the reason why the Msbuild variables not work in Project Extensions.
Hope this helps.
You could update the underlying project file. A Target like this in your project file would do it.
<Target Name="AfterBuild">
<PropertyGroup>
<NewUrl>http://localhost/api/$(CustomerName)</NewUrl>
</PropertyGroup>
<Message Text="Updating IISUrl: $(NewUrl) in $(MSBuildProjectFile)" />
<XmlPeek Namespaces="<Namespace Prefix='msb' Uri='http://schemas.microsoft.com/developer/msbuild/2003'/>" XmlInputPath="$(MSBuildProjectFile)" Query="/msb:Project/msb:ProjectExtensions/msb:VisualStudio/msb:FlavorProperties/msb:WebProjectProperties/msb:IISUrl/text()">
<Output TaskParameter="Result" ItemName="Peeked" />
</XmlPeek>
<Message Text="Current Url: #(Peeked)" />
<!-- Only update the IISUrl if its changed -->
<XmlPoke Condition=" '#(Peeked)'!='$(NewUrl)' " XmlInputPath="$(MSBuildProjectFile)" Namespaces="<Namespace Prefix='msb' Uri='http://schemas.microsoft.com/developer/msbuild/2003'/>" Query="/msb:Project/msb:ProjectExtensions/msb:VisualStudio/msb:FlavorProperties/msb:WebProjectProperties/msb:IISUrl" Value="$(NewUrl)" />
</Target>
However it does have side affects. Changing the underlying project file means Visual Studio decides it should reload the project.
To use it you cannot go directly into Debug. Instead build, reload the project and then go into debug. If you go directly into Debug (with a compile) it will use the old url.

get solution path at build time or runtime

I have a C# solution that I would like to get the path of the solution be set to the app.config during build time. for instance. Lets say I have the solutions c:\temp\visual studio\super fun project\super_fun_project.sln open. I build and in one of the test projects a app setting is changed to be the full path of the solution. ie
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="fullSolutionPath" value="{setAtBuild}"/>
</appSettings>
</configuration>
that if I were to go to c:\temp\visual studio\super fun project\Foobar.Tests\bin\Debug\Foobar.Tests.dll.config it would look be
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="fullSolutionPath" value="c:\temp\visual studio\super fun project\super_fun_project.sln"/>
</appSettings>
</configuration>
or however it needs to get formated so that when at runtime I ask for the value I indeed get the correct path. I've looked down Transformation, but I can't figure out how I would get the solution path set. Are there any other tricks to get this?
What you can do is modify the project file and add an MsBuild Target.
The target can use a Custom Inline Task, a task with its source code integrated into the project file.
So to add this task:
1) unload the project (right click on project node, select "Unload Project")
2) edit the project file (right click on project node, select "Edit ")
3) add the following to the project file (for example to the end) and reload it, now when you build, the config file will be modified accordingly.
<Project ...>
...
<Target Name="AfterBuild">
<RegexReplace FilePath="$(TargetDir)$(TargetFileName).config" Input="setAtBuild" Output="$(SolutionPath)" />
</Target>
<UsingTask TaskName="RegexReplace" TaskFactory="CodeTaskFactory" AssemblyName="Microsoft.Build.Tasks.Core" >
<ParameterGroup>
<FilePath Required="true" />
<Input Required="true" />
<Output Required="true" />
</ParameterGroup>
<Task>
<Using Namespace="System.Text.RegularExpressions"/>
<Code Type="Fragment" Language="cs"><![CDATA[
File.WriteAllText(FilePath, Regex.Replace(File.ReadAllText(FilePath), Input, Output));
]]></Code>
</Task>
</UsingTask>
</Project>
Here, I've defined Output to use a Visual Studio's MSBuild Property named SolutionPath, but you can reuse this RegexReplace task and update Input and Output parameters to various needs.
I dont know what your use case is but you can call a homegrown batch file to do this from the post-build events of your project.
example: create a batch script in your project entitled 'updateconf.bat', ensure that it is ANSII encoded (maybe use notepad++ to write the script and confirm ansii) or you'll get an exception indicating that the file is prefixed with an illegal character when you compile your VS project and check the output.
Contents of batch script:
#echo off > newfile & setLocal ENABLEDELAYEDEXPANSION
set old="{setAtBuild}"
set new=%2
set targetBinary=%3
cd %1
for /f "tokens=* delims= " %%a in (%targetBinary%.config) do (
set str=%%a
set str=!str:%old%=%new%!
>> newfile echo !str!
)
del /F /Q %targetBinary%.config
rename "newfile" "%targetBinary%.config"
Then add a post-build event in your project properties that calls the batch script:
call $(ProjectDir)\updateconf.bat "$(TargetDir)" "$(SolutionPath)" $(TargetFileName)

MSBuild - XPath - XmlPeek can read but XmlPoke can not write

I'm using MSBuild to manipulate my Project (.csproj) file to update a reference to a static file. The static file will be built by my CI Server (TeamCity) and then the reference the Project uses will need to be updated before the Project itself is built.
Here is an example of the Xml from my csproj file (full version):
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
ToolsVersion="12.0">
<ItemGroup>
<Content Include="packages\pMixins.0.1.7.nupkg">
<IncludeInVSIX>true</IncludeInVSIX>
</Content>
I have written an MSBuild Task:
<Target Name="ReplaceNugetPackageDependency" BeforeTargets="PrepareForBuild" >
<XmlPoke
XmlInputPath="$(MSBuildProjectFile)"
Query="//n:Project/n:ItemGroup/
n:Content[starts-with(#Include, 'packages')]/#Include"
Value="TEST-TEST"
Namespaces="<Namespace Prefix='n'
Uri='http://schemas.microsoft.com/developer/msbuild/2003'
Name='DoNotKnowWhatThisIsFor-ButItIsRequired' />" >
</XmlPoke>
</Target>
But when I run it I get the message 0 replacements.
So I added an XmlPeek task to test the query:
<XmlPeek
XmlInputPath="$(MSBuildProjectFile)"
Query="/n:Project/n:ItemGroup/
n:Content[starts-with(#Include, 'packages')]/#Include"
Namespaces="<Namespace Prefix='n'
Uri='http://schemas.microsoft.com/developer/msbuild/2003'
Name='DoNotKnowWhatThisIsFor-ButItIsRequired' />">
<Output TaskParameter="Result" ItemName="Peeked" />
</XmlPeek>
<Message Text="Text: #(Peeked)"/>
When I run MSBuild XmlPeek is able to read the Xml:
Text: packages\pMixins.0.1.7.nupkg
The queries are exactly the same! Why can't XmlPoke manipulate the Xml if XmlPeek can read it?
UPDATE
After hours of playing with this, I finally found an XPath query that will get XmlPoke to do what I want:
Query="//n:Project/n:ItemGroup/
n:Content[starts-with(#Include, 'packages')]/n:IncludeInVSIX/../#Include"
Why is it necessary to add /n:IncludeInVSIX/..? Is this a bug??
Just wanted to confirm for anyone else who encounters this, that this is how, in fact, you get around the issue of not being able to use the same exact XPath query in XmlPeek task and XmlPoke task.
Original query to replace "file" attribute value of AppSettings element in regular web.config:
<appSettings file="devsettings.config">
<add key="BuildVersion" value="" />
</appSettings>
To get at the "file" attribute in XmlPeek task I used following XPath query:
//appSettings[#file='devsettings.config']/#file
However this same query wouldn't work in XmlPoke task. Instead the following worked like what #philip-pittle discovered in his update to the question
//appSettings[#file='devsettings.config']/add/../#file
<XmlPeek XmlInputPath="$(_BuildPath)web.config"
Query="//appSettings[#file='devsettings.config']/#file">
<Output TaskParameter="Result" ItemName="ExistingPeeked" />
</XmlPeek>
<XmlPoke XmlInputPath="$(_BuildPath)web.config"
Query="//appSettings[#file='devsettings.config']/add/../#file"
Value="$(_EnvironmentConfig)" />
This was using following versions of MSBuild.
Microsoft (R) Build Engine version 12.0.31101.0
[Microsoft .NET Framework, version 4.0.30319.18444]
May be it's a bug? But definitely odd behavior.

Building sub projects using NANT

I'm trying to build a project(A) using NANT. The project(A) relies upon another project(B) which is also built with NANT. I want to be able to invoke the build of the dependent project(B) from within the build of project(A). I've tried including the build file of project B in the build file of project A. This creates an error because the two build files contain targets that share the same name.
Is there a way to alias the included build file?
You can do it like this by creating a "parent" buildfile, that uses the "nant" action to call other buildfiles.
<target name="rebuild" depends="" >
<nant target="${target::get-current-target()}">
<buildfiles>
<include name="projectB.build" />
<include name="projectC.build" />
</buildfiles>
</nant>
</target>
I was trying to do this using an include task but have found that the nant task is actually what I required.
You can have several of such targets in your 'master' file. I often use the following construction to share a set of build files between targets to make script maintenance easier.
<fileset id="buildfiles.all">
<include name="projectB.build"/>
<include name="projectB.build"/>
</fileset>
<target name="build">
<nant target="${target::get-current-target()}">
<buildfiles refid="buildfiles.all" />
</nant>
</target>
<target name="clean">
<nant target="${target::get-current-target()}">
<buildfiles refid="buildfiles.all" />
</nant>
</target>
<target name="publish">
<nant target="${target::get-current-target()}">
<buildfiles refid="buildfiles.all" />
</nant>
</target>

How to synchronise the publish version to the assembly version in a .NET ClickOnce application?

In my C# ClickOnce application, there is an auto-incremented publish version in the Project -> Properties -> Publish tab. I'd like to display that version in my menu Help -> About box, but the code I'm using apparently accesses the assembly Version, which is different.
The Assembly Version can be changed manually in the Project -> Properties -> Application -> Assembly Information dialog. So for now, every time before I publish I've been copying the publish version to the assembly version, so my dialog shows the current version of
the application. There must be a better way to do this.
All I really want to do is have an accurate, auto-updated, code-accessible version number.
Here's the code I'm using to access the assembly version number:
public string AssemblyVersion
{
get
{
return Assembly.GetExecutingAssembly().GetName().Version.ToString();
}
}
An alternative would be to find code that accesses the publish version.
sylvanaar's last line looks like the way to go, in my experience; but with the caveat that it is only available to deployed versions of the application. For debugging purposes, you might want something like:
static internal string GetVersion()
{
if (ApplicationDeployment.IsNetworkDeployed)
{
return ApplicationDeployment.CurrentDeployment.CurrentVersion.ToString();
}
return "Debug";
}
I modified my .csproj file to update the assembly version. I created a configuration called "Public Release" for this, but it's not required to do that.
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
<PropertyGroup Condition="'$(BuildingInsideVisualStudio)' == 'true'">
<MSBuildCommunityTasksPath>$(SolutionDir)Tools\MSBuildCommunityTasks</MSBuildCommunityTasksPath>
</PropertyGroup>
<!-- Required Import to use MSBuild Community Tasks -->
<Import Project="$(SolutionDir)Tools\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" Condition="'$(BuildingInsideVisualStudio)' == 'true'" />
<Target Name="BeforeCompile" Condition="'$(BuildingInsideVisualStudio)|$(Configuration)' == 'true|PublicRelease'">
<FormatVersion Version="$(ApplicationVersion)" Revision="$(ApplicationRevision)">
<Output TaskParameter="OutputVersion" PropertyName="AssemblyVersionToUse" />
</FormatVersion>
<AssemblyInfo CodeLanguage="CS" OutputFile="$(ProjectDir)Properties\VersionInfo.cs" AssemblyVersion="$(AssemblyVersionToUse)" AssemblyFileVersion="$(AssemblyVersionToUse)" />
</Target>
The published version may be:
ApplicationDeployment.CurrentDeployment.CurrentVersion
I would like to expand on Sylvanaar's answer, as some of implementation details weren't obvious to me. So:
Manually install community build tasks found at: https://github.com/loresoft/msbuildtasks/releases Note: Don't install by nuget if you clean your packages, as the build will fail before getting a chance to restore the packages, since msbuildtasks are referenced as a task in the build file. I put these in folder next to solution file called .build
Add a completely empty file to your projects properties folder called VersionInfo.cs
3 Remove these lines if they exist in AssemblyInfo.cs
[assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyFileVersion("1.0.*")]
4 Modify your csproj file
<!-- Include the build rules for a C# project. -->
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!--INSERT STARTS HERE-->
<!--note the use of .build directory-->
<PropertyGroup Condition="'$(BuildingInsideVisualStudio)' == 'true'">
<MSBuildCommunityTasksPath>$(SolutionDir)\.build\MSBuildCommunityTasks</MSBuildCommunityTasksPath>
</PropertyGroup>
<!-- Required Import to use MSBuild Community Tasks -->
<Import Project="$(SolutionDir)\.build\MSBuild.Community.Tasks.targets" Condition="'$(BuildingInsideVisualStudio)' == 'true'" />
<Target Name="BeforeCompile" Condition="'$(BuildingInsideVisualStudio)|$(Configuration)' == 'true|Release'">
<FormatVersion Version="$(ApplicationVersion)" Revision="$(ApplicationRevision)">
<Output TaskParameter="OutputVersion" PropertyName="AssemblyVersionToUse" />
</FormatVersion>
<AssemblyInfo CodeLanguage="CS" OutputFile="$(ProjectDir)Properties\VersionInfo.cs" AssemblyVersion="$(AssemblyVersionToUse)" AssemblyFileVersion="$(AssemblyVersionToUse)" />
</Target>
5 Use a method like the following to access the version text:
public string Version()
{
Version version = null;
if (ApplicationDeployment.IsNetworkDeployed)
{
version = ApplicationDeployment.CurrentDeployment.CurrentVersion;
}
else
{
version = typeof(ThisAddIn).Assembly.GetName().Version;
}
return version.ToString();
}
I modified sylvanaar's solution for use with VB:
- Microsoft.VisualBasic.targets instead of Microsoft.CSharp.targets
- CodeLanguage="VB" instead of CodeLanguage="CS"
- AssemblyInfo.vb instead of VersionInfo.cs
, differences in paths:
- $(SolutionDir).build instead of $(SolutionDir)Tools\MSBuildCommunityTasks
- $(ProjectDir)AssemblyInfo.vb instead of $(ProjectDir)Properties\VersionInfo.cs
, and to remove conditions:
- Condition="'$(BuildingInsideVisualStudio)' == 'true'"
- Condition="'$(BuildingInsideVisualStudio)|$(Configuration)' == 'true|PublicRelease'"
I also synchronized Company and Product with ClickOnce PublisherName and ProductName respectively and generated a Copyright based on the current date:
- AssemblyCompany="$(PublisherName)"
- AssemblyProduct="$(ProductName)"
- AssemblyCopyright="© $([System.DateTime]::Now.ToString(`yyyy`)) $(PublisherName)"
I ended up adding this to my vbproj file. It requires the MSBuildTasks NuGet package to be installed first:
<Import Project="$(MSBuildBinPath)\Microsoft.VisualBasic.targets" />
<PropertyGroup Condition="'$(BuildingInsideVisualStudio)' == 'true'">
<MSBuildCommunityTasksPath>$(SolutionDir).build</MSBuildCommunityTasksPath>
</PropertyGroup>
<Import Project="$(MSBuildCommunityTasksPath)\MSBuild.Community.Tasks.Targets" Condition="'$(BuildingInsideVisualStudio)' == 'true'" />
<Target Name="BeforeCompile">
<FormatVersion Version="$(ApplicationVersion)" Revision="$(ApplicationRevision)">
<Output TaskParameter="OutputVersion" PropertyName="AssemblyVersionToUse" />
</FormatVersion>
<AssemblyInfo CodeLanguage="VB" OutputFile="$(ProjectDir)AssemblyInfo.vb" AssemblyVersion="$(AssemblyVersionToUse)" AssemblyFileVersion="$(AssemblyVersionToUse)" AssemblyCompany="$(PublisherName)" AssemblyProduct="$(ProductName)" AssemblyCopyright="© $([System.DateTime]::Now.ToString(`yyyy`)) $(PublisherName)"/>
</Target>
I'm not sure how much the location within the project file matters, but I added this to the end of my project file, just before:
</Project>
I did it the other way around, used a wildcard for my assembly version - 1.0.* - so Visual Studio/MSBuild generated a version number automatically:
// AssemblyInfo.cs
[assembly: AssemblyVersion("1.0.*")]
And then I added the following AfterCompile target to the ClickOnce project to assign synchronize PublishVersion with the assembly version:
<Target Name="AfterCompile">
<GetAssemblyIdentity AssemblyFiles="$(IntermediateOutputPath)$(TargetFileName)">
<Output TaskParameter="Assemblies" ItemName="TargetAssemblyIdentity" />
</GetAssemblyIdentity>
<PropertyGroup>
<PublishVersion>%(TargetAssemblyIdentity.Version)</PublishVersion>
</PropertyGroup>
</Target>

Categories

Resources