MSBuild removing backslash in Directory field from batch processing of ItemGroup - msbuild

I have a .csproj file I'm trying to configure to perform a build of a couple of projects, map drives to point to the output of the projects along with some support documents, build an installer, then unmap the drives. I keep getting erros when I call subst to map the drive due to a trailing backslash in path. Here's the building and mapping targets
<Target Name="BuildApps">
<MSBuild Projects="..\MsApp\MsApp.csproj; ..\AvApp\AvApp.csproj"
Targets="Build">
<Output TaskParameter="TargetOutputs"
ItemName="PackageOutput"
/>
</MSBuild>
</Target>
<Target Name="MapDrives">
<Exec Condition="'%(PackageOutput.Filename)' == 'MsApp'" Command="subst O: "%(PackageOutput.Rootdir)%(PackageOutput.Directory)"" WorkingDirectory="C:" ContinueOnError="true" />
<Exec Condition="'%(PackageOutput.Filename)' == 'AvApp'" Command="subst P: "%(PackageOutput.Rootdir)%(PackageOutput.Directory)"" WorkingDirectory="C:" ContinueOnError="true" />
</Target>
In researching the problem, I came across this question on removing the backslash. When I modify the target to
<Target Name="MapDrives">
<PropertyGroup Condition="'%(PackageOutput.Filename)' == 'MsApp'">
<MsDirectory>%(PackageOutput.Directory)</MsDirectory>
</PropertyGroup>
<PropertyGroup Condition="'%(PackageOutput.Filename)' == 'AvApp'">
<AvDirectory>%(PackageOutput.Directory)</AvDirectory>
</PropertyGroup>
<Exec Condition="'%(PackageOutput.Filename)' == 'MsApp'" Command="subst O: "%(PackageOutput.Rootdir)$(MsDirectory.TrimEnd('\'))"" WorkingDirectory="C:" ContinueOnError="true" />
<Exec Condition="'%(PackageOutput.Filename)' == 'AvApp'" Command="subst P: "%(PackageOutput.Rootdir)$(AvDirectory.TrimEnd('\'))"" WorkingDirectory="C:" ContinueOnError="true" />
</Target>
I get the error
error MSB4190: The reference to the built-in metadata "Filename" at position 1 is not allowed in this condition "'%(PackageOutput.Filename)' == 'MsApp'"
Researching this error, I ran across this and the fact that properties are evaluated before items which may play a factor in the error. So, my question is there an easy way to remove the backslash by either converting this item list to a property or a way to perform a string operation on an item?

I managed to get this to work with the following syntax:
<Exec Condition="'%(PackageOutput.Filename)' == 'MsApp'" Command="subst O: "#(PackageOutput->Replace('\MsApp.exe', ''))"" WorkingDirectory="C:" ContinueOnError="true" />
<Exec Condition="'%(PackageOutput.Filename)' == 'AvApp'" Command="subst P: "#(PackageOutput->Replace('\AvApp.exe', ''))"" WorkingDirectory="C:" ContinueOnError="true" />

Related

MSBuild Update property in csproj

I have been trying to update the ApplicationVersion property in my csproj file.witch works fine; i have added a Target that runs an custom task to extract the AssemblyFileVersion from my assemblyinfo.cs; this works there is no doubt about that.
But then when i want to use my updated ApplicationVersion to determan where to put my newly build files, i get the default value set in the property.
<PropertyGroup>
...
<ApplicationVersion>1.0.0.0</ApplicationVersion>
...
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\media-converter-BUILD\debug\$(ApplicationVersion)\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<DocumentationFile>..\media-converter-BUILD\debug\$(ApplicationVersion)\MediaConverter.XML</DocumentationFile>
</PropertyGroup>
My Targets
<UsingTask AssemblyFile="GetAssemblyFileVersion.dll" TaskName="GetAssemblyFileVersion.GetAssemblyFileVersion" />
<Target Name="MainAfterCompile">
<CallTarget Targets="AfterCompile" />
<CallTarget Targets="VerifyParam" />
</Target>
<Target Name="AfterCompile">
<GetAssemblyFileVersion strFilePathAssemblyInfo="Properties\AssemblyInfo.cs">
<Output TaskParameter="strAssemblyFileVersion" PropertyName="ApplicationVersionModded" />
</GetAssemblyFileVersion>
<PropertyGroup>
<ApplicationVersion>$(ApplicationVersionModded)</ApplicationVersion>
</PropertyGroup>
</Target>
<Target Name="VerifyParam">
<Message Text="New $(ApplicationVersionModded)" Importance="high"/>
<Message Text="Old Updated $(ApplicationVersion)" Importance="high"/>
</Target>
the GetAssemblyFileVersion.dll i more or less stole from some post i found on the internet, just can't find it again, so i can't add a link, sorry.
My theory on why it does not work is that the transforms and parameters in PropertyGroups are rendered before both InitailTagets and DefaultTargets is run. And there for will my plan never work
but if anyone knows of a way to make it work, i will be grateful to here it
My theory on why it does not work is that the transforms and parameters in PropertyGroups are rendered before both InitailTagets and DefaultTargets is run indeed, that's how the evaluation order works: msbuild evaluates global properties in the first pass of the file, you define OutputPath, that is used by the Microsoft.Common.CurrentVersion.targets file to derive OutDir/BaseIntermediateOutputPath/.... Then in another pass your targets run and update the version number, but there isn't another pass which evaluates the global OutputPath property again.
You can however override the value of OutputPath and derived paths in a Target, and it will take effect, you just have to take care of running it early in the build so that other targets use the updated version. This does the trick:
<Target Name="GetApplicationVersion">
<GetAssemblyFileVersion strFilePathAssemblyInfo="Properties\AssemblyInfo.cs">
<Output TaskParameter="strAssemblyFileVersion" PropertyName="ApplicationVersion" />
</GetAssemblyFileVersion>
</Target>
<Target Name="SetOutputPaths" DependsOnTargets="GetApplicationVersion"
BeforeTargets="PrepareForBuild">
<PropertyGroup>
<OutputPath>bin\$(Configuration)\$(ApplicationVersion)\</OutputPath>
<OutDir>$(OutputPath)</OutDir>
</PropertyGroup>
<Message Text="Set OutDir to $(OutDir)" Importance="high" />
</Target>
Another way to deal with this is doing things the other way around: define the application version as a global msbuild property, then use it to define OutputPath and to update the number in AssemblyVersion.cs before it is compiled.

MSBuild backup files (with time stamp) before replacing

I currently use a web deployment project to update an existing web site through various MSBuild tasks
Taking site offline:
<Target Name="BeforeBuild" Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<CreateItem Include="..\website\App_Code\override\App_Offline.htm">
<Output TaskParameter="Include" ItemName="AppOffline" />
</CreateItem>
<Copy SourceFiles="#(AppOffline)" DestinationFiles="#(AppOffline->'\\servername\f$\web\example.com\wwwroot\%(RecursiveDir)%(Filename)%(Extension)')" />
<RemoveDir Directories="\\servername\f$\web\example.com\wwwroot\bin\" />
</Target>
Update files:
<Target Name="AfterBuild" Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<CreateItem Include=".\Release\**\*">
<Output TaskParameter="Include" ItemName="ReleaseFiles" />
</CreateItem>
<Copy SourceFiles="#(ReleaseFiles)" ContinueOnError="true" SkipUnchangedFiles="true" DestinationFiles="#(ReleaseFiles->'\\servername\f$\web\example.com\wwwroot\%(RecursiveDir)%(Filename)%(Extension)')" />
<Delete Files="\\servername\f$\web\example.com\wwwroot\App_Offline.htm" />
</Target>
However, what I also want to do is backup the existing site before overwriting anything, into a timestamped zip (or 7zip) file, e.g. if done on 3rd October 2011 at 10:35 file would be named \\servername\f$\web\example.com\backup201110031035.7z containing the contents of the wwwroot folder (before taking the site offline and deleting the bin directory)
The MSBuild Community Tasks provide support for both, zipping and getting the (formatted) current time through the Zip and Time tasks respectively.
Once the MSBuild community tasks are installed and imported to your project, you can create a backup zip file by adding the following to the beginning of your BeforeBuild target.
<ItemGroup>
<FilesToZip Include="\\servername\f$\web\example.com\wwwroot\**\*.*" />
</ItemGroup>
<Time Format="yyyyMMddhhmm">
<Output TaskParameter="FormattedTime" PropertyName="Timestamp" />
</Time>
<Zip
Files="#(FilesToZip)"
ZipFileName="\\servername\f$\web\example.com\backup$(Timestamp).zip"
WorkingDirectory="\\servername\f$\web\example.com\wwwroot" />

Web Deployment Project / MSBuild - TempBuildDir

When using the Web Deployment Project MSBuild uses the folder '.TempBuildDir' when performing the build. Is it possible to specify an alternative folder?
In C:\Program Files\MSBuild\Microsoft\WebDeployment\v9.0 or v10.0 directory is the Microsoft.WebDeployment.targets file where the TempBuildDir property is defined in the _PrepareForBuild target.
Since they use the CreateProperty task to set TempBuildDir it is always set to the hard-coded value even if the property already exists. This could be to eliminate the problem of someone using TempBuildDir property for something else and messing up the build.
You would have to change the Microsoft.WebDeployment.targets file to use a different temp directory.
WARNING: The following is changing a file you don't have control over so use are your own risk.
If you were to change the following lines in the _PrepareForBuild target from
<CreateProperty Value=".\TempBuildDir\">
<Output TaskParameter="Value" PropertyName="TempBuildDir" />
</CreateProperty>
to
<CreateProperty Value="$(MySpecialWebTempBuildDir)" Condition=" '$(MySpecialWebTempBuildDir)' != '' ">
<Output TaskParameter="Value" PropertyName="TempBuildDir" />
</CreateProperty>
<CreateProperty Value=".\TempBuildDir\" Condition=" '$(MySpecialWebTempBuildDir)' == '' ">
<Output TaskParameter="Value" PropertyName="TempBuildDir" />
</CreateProperty>
Then set the MySpecialWebTempBuildDir property in your project file and it should override it. If you don't set MySpecialWebTempBuildDir then it will use TempBuildDir as before.
If you install an update to the web deployment package your changes will get overwritten.
Another solution is to uncomment and override the "BeforeBuild" target of the web deployment project as follows:
<Target Name="BeforeBuild">
<CreateProperty Value=".\TempBuildDirDebug\" Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<Output TaskParameter="Value" PropertyName="TempBuildDir" />
</CreateProperty>
<CreateProperty Value=".\TempBuildDirRelease\" Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<Output TaskParameter="Value" PropertyName="TempBuildDir" />
</CreateProperty>

How to invoke the same msbuild target twice with different parameters from within msbuild project file itself

I have the following piece of msbuild code:
<PropertyGroup>
<DirA>C:\DirA\</DirA>
<DirB>C:\DirB\</DirB>
</PropertyGroup>
<Target Name="CopyToDirA"
Condition="Exists('$(DirA)') AND '#(FilesToCopy)' != ''"
Inputs="#(FilesToCopy)"
Outputs="#(FilesToCopy -> '$(DirA)%(Filename)%(Extension)')">
<Copy SourceFiles="#(FilesToCopy)" DestinationFolder="$(DirA)" />
</Target>
<Target Name="CopyToDirB"
Condition="Exists('$(DirB)') AND '#(FilesToCopy)' != ''"
Inputs="#(FilesToCopy)"
Outputs="#(FilesToCopy -> '$(DirB)%(Filename)%(Extension)')">
<Copy SourceFiles="#(FilesToCopy)" DestinationFolder="$(DirB)" />
</Target>
<Target Name="CopyFiles" DependsOnTargets="CopyToDirA;CopyToDirB"/>
So invoking the target CopyFiles copies the relevant files to $(DirA) and $(DirB), provided they are not already there and up-to-date.
But the targets CopyToDirA and CopyToDirB look identical except one copies to $(DirA) and the other - to $(DirB). Is it possible to unify them into one target first invoked with $(DirA) and then with $(DirB)?
Thanks.
You should be able to generate an ItemGroup containing the Dirs and then % on that.
<ItemGroup>
<Dirs Include="C:\DirA\;C:\DirB\">
</ItemGroup>
<Target Name="CopyFiles"
Condition="Exists('%(Dirs)') AND '#(FilesToCopy)' != ''"
Inputs="#(FilesToCopy)"
Outputs="#(FilesToCopy -> '%(Dirs)%(Filename)%(Extension)')">
<Copy SourceFiles="#(FilesToCopy)" DestinationFolder="%(Dirs)" />
</Target>
Or you can do 2 explicit calls:
<Target Name="CopyFiles">
<MsBuild Projects="$(MSBuildProjectFullPath)" Targets="CopyASetOfFiles" Properties="FilesToCopy=#(FilesToCopy);DestDir=$(DirA)" />
<MsBuild Projects="$(MSBuildProjectFullPath)" Targets="CopyASetOfFiles" Properties="FilesToCopy=#(FilesToCopy);DestDir=$(DirB)" />
</Target>
<Target Name="CopyASetOfFiles"
Condition="Exists('$(DestDir)') AND '#(FilesToCopy)' != ''"
Inputs="#(FilesToCopy)"
Outputs="#(FilesToCopy -> '$(DestDir)%(Filename)%(Extension)')">
<Copy SourceFiles="#(FilesToCopy)" DestinationFolder="$(DestDir)" />
</Target>
I haven't tested either syntax, but am relatively more confident of the second.
(The answer, if there is one, is in my Sayed Hashimi book on my desk - you'll have to wait until the first of:
Get the book
I get bored
Sayed finds this post and comes up with a brilliant tested answer)
As someone already mentiond the answer is batching.
Here are some links:
MSBuild Batching Part 1
MSBuild Batching Part 2
MSBuild Batching Part 3
MSBuild RE: Enforcing the Build Agent in a Team Build
Yes, what you want is called batching in MSBuild. The
;%(Dirs.Identity)
Defined in the Outputs will cause this task to be executed for each item in the Dirs ItemGroup.
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="CopyFiles"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
ToolsVersion="3.5">
<ItemGroup>
<Dirs Include="C:\DirA" />
<Dirs Include="C:\DirB" />
</ItemGroup>
<Target Name="CopyFiles"
Inputs="#(FilesToCopy);#(Dirs)"
Outputs="#(FilesToCopy -> '%(Dirs.Identity)%(Filename)%(Extension)');%(Dirs.Identity)" >
<Message Text="%(Dirs.Identity)" />
</Target>
</Project>
Outputs:
Build started 8/19/2009 10:11:57 PM.
Project "D:\temp\test.proj" on node 0 (default targets).
C:\DirA
CopyFiles:
C:\DirB
Done Building Project "D:\temp\test.proj" (default targets).
Change the Message task to Copy task with the following condition and you are done:
Condition="Exists('%(Dirs.Identity)') AND '#(FilesToCopy)' != ''"

MSBuild Copy task not copying files the first time round

I created a build.proj file which consists of a task to copy files that will be generated after the build is complete. The problem is that these files are not copied the first time round and I have to run msbuild again on the build.proj so that the files can be copied. Please can anyone tell me whats wrong with the following build.proj file:
<Configuration Condition="'$(Configuration)' == ''">Debug</Configuration>
<SourcePath Condition="'$(SourcePath)' == ''">$(MSBuildProjectDirectory)</SourcePath>
<BuildDir>$(SourcePath)\build</BuildDir>
</PropertyGroup>
<ItemGroup>
<Projects
Include="$(SourcePath)\src\myApp\application.csproj">
</Projects>
</ItemGroup>
<Target Name="Build">
<Message text = "Building project" />
<MSBuild
Projects="#(Projects)"
Properties="Configuration=$(Configuration)" />
</Target>
<ItemGroup>
<OutputFiles Include ="$(MSBuildProjectDirectory)\**\**\bin\Debug\*.*"/>
</ItemGroup>
<Target Name="CopyToBuildFolder">
<Message text = "Copying build items" />
<Copy SourceFiles="#(OutputFiles)" DestinationFolder="$(BuildDir)"/>
</Target>
<Target Name="All"
DependsOnTargets="Build; CopyToBuildFolder"/>
</Project>
The itemgroups are evaluated when the script is parsed. At that time your files aren't there yet. To be able to find the files you'll have to fill the itemgroup from within a target.
<!-- SQL Scripts which are needed for deployment -->
<Target Name="BeforeCopySqlScripts">
<CreateItem Include="$(SolutionRoot)\04\**\Databases\**\*.sql">
<Output ItemName="CopySqlScript" TaskParameter="Include"/>
</CreateItem>
</Target>
This example creates the ItemGroup named "CopySqlScript" using the expression in the Include attribute.
Edit:
Now I can read your script: add the CreateItem tag within your CopyToBuildFolder target