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

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.

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.

Is it possile to sent build number to csproj as a parameter without using the Build.proj

I am not pretty familiar with msbuild technique.
Currently I have a build.proj in my solution to assign a build number to the exe, by using a following param:
<PropertyGroup Condition=" '$(BUILD_NUMBER)' != '' ">
<!-- Build Server Number -->
<Version>$(BUILD_NUMBER)Version>
<FileVersion>$(BUILD_NUMBER)FileVersion>
<InformationalVersion>$(BUILD_NUMBER)InformationalVersion></PropertyGroup><Target Name="Version">
<Attrib Files="$(MSBuildProjectDirectory)\AssemblyInfo.cs" ReadOnly="False" />
<AssemblyInfo CodeLanguage="CS"
OutputFile="$(MSBuildProjectDirectory)\GlobalAssemblyInfo.cs"
GenerateClass="true"
AssemblyCopyright="Copyright © $(Year). All rights reserved."
AssemblyConfiguration="$(BuildConfiguration)"
AssemblyVersion="$(Version)"
AssemblyFileVersion="$(FileVersion)"
AssemblyInformationalVersion="$(InformationalVersion)" /> </Target>
Now I can't use build.proj. Is it any alternative way to assign a build number to csproj?
We generate a AssemblyInfo.cs file to put in all assembly attributes we need to define. You could do this on pre-build time if that suits you. There we define the AssemblyVersion, AssemblyFileVersion attribute and other related attributes.
Those properties will be used compiling the assembly.

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)

Dynamic Versioning

I have a situation where i want the versioning to be dynamic at build time.
Version Pattern: <year>.<month>.<day>.<hhmm>
But i have read where the String value used in the Attribute is reparsed at compile time.
Any advise on how to get this dynamic versioning completed?
Ideal situation:
<Assembly: AssemblyVersion("4.0.0.0")>
<Assembly: AssemblyFileVersion(Year(Now) & "." & Month(Now()) & "." & Day(Now()) & "." & String.format("hhmm", now()))>
I know it wont work but should get the point acrossed.
You can use the MsbuildCommunityTasks to generate the build number and to customize the assembly file version on pre-build time.
Download the zip at MsbuildCommunityTasks
Unzip to the folder [SolutionFolder]\MsBuildExtensions\MSBuildCommunityTasks
Add the sample below on your project (csproj), just after the Microsoft.CSharp.Targets import.
<PropertyGroup>
<MSBuildCommunityTasksPath>$(MSBuildThisFileDirectory)..\MsBuildExtensions\MSBuildCommunityTasks</MSBuildCommunityTasksPath>
<My-PropertiesDir>Properties</My-PropertiesDir>
</PropertyGroup>
<Import Project="$(MSBuildCommunityTasksPath)\MSBuild.Community.Tasks.Targets"/>
<Target Name="BeforeBuild">
<Time Format="yyyy.MM.dd.HHmm">
<Output TaskParameter="FormattedTime" PropertyName="My-VersionNumber" />
</Time>
<Message Text="Building $(My-VersionNumber) ...">
</Message>
<ItemGroup>
<My-AssemblyInfo Include="$(My-PropertiesDir)\AssemblyVersionInfo.cs" />
<Compile Include="#(My-AssemblyInfo)" />
</ItemGroup>
<MakeDir Directories="$(My-PropertiesDir)" />
<AssemblyInfo OutputFile="#(My-AssemblyInfo)"
CodeLanguage="CS"
AssemblyFileVersion="$(My-VersionNumber)"
AssemblyInformationalVersion="$(My-VersionNumber)"
Condition="$(My-VersionNumber) != '' "
/>
</Target>
<Target Name="AfterBuild">
<Delete Files="#(My-AssemblyInfo)" />
</Target>
Wipe the AssemblyFileVersion attribute from your AssemblyInfo.cs. It will be generated at build time.
You'll see the version number being printed on the console when you build. The generated file is deleted on the AfterBuild target, to keep your source control clean.
BeforeBuild:
Building 2013.01.14.1016 ...
Created AssemblyInfo file "Properties\AssemblyVersionInfo.cs".
(...)
AfterBuild:
Deleting file "Properties\AssemblyVersionInfo.cs".
If you want do this to many projects with less msbuild code, it will be necessary to create a customized build script to wrap up your solution.

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

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.

Categories

Resources