MSBuild ITaskItem array is out of date - msbuild

I'm creating a custom ITask for MSBuild which uploads the output files of my build. I'm using a web deployment project to publish my app and hooking in to the AfterBuild target to do my custom work.
If I add a file to my web application, the first time I do a build, my custom task is not recognizing the recently added file. In order for that file to show up in my array of ITaskItems, I have to first do a build with my 'AfterBuild' target removed and then start the build again with my 'AfterBuild' target in place.
Here is what my build file looks like:
<ItemGroup>
<PublishContent Include="$(OutputPath)\**" />
</ItemGroup>
<Target Name="AfterBuild">
<UploadTask FilesToPublish="#(PublishContent)" />
</Target>
The list in #(PublishContent) seems to be initialized at the beginning of the build instead of reflecting any changes that might have taken place by the build process itself.
Any ideas?
Thanks

Your ItemGroup is going to get evaluated when the project file first loads (when you first open Visual Studio or you 'unload' and 'reload' the project in Solution Explorer).
What you probably need is to create an ItemGroup as a task in your 'AfterBuild' target. Example:
<CreateItem Include="$(OutputPath)\**">
<Output TaskParameter="Include" ItemName="OutputFiles"/>
</CreateItem>
followed by:
<Target Name="AfterBuild">
<UploadTask FilesToPublish="#(OutputFiles)" />
</Target>

Related

Pass property for single project on commandline when building a solution

When I use msbuild to build a solution containing multiple projects, can I pass a property on the command line in a way that the property will only be used for one of the projects?
That means, can I say that -p:Foo=42 shall be used for Project1, but not for Project2?
MSBuild cannot specify a property for one of the project by the solution file(msbuild xxx\xxx.sln) easily and only targets can be specified. Or you have to type multiple msbuild command line to specify to the related csproj file to enable that changed property like msbuild Project1.csproj -t:build -p:xxx=xxx, msbuild Project2.csproj -t:build. However, it is too complex and inconvenient.
So I recommend that you could use msbuild script to get what you want.
1) create a file called build.proj file:
<Project>
<ItemGroup>
<!--add all the projects from the solution and remove the any project you want to modify the foo property-->
<MostProjectFile Include="**\*.csproj;**\*.vcxproj" Exclude="**\Project1.csproj" />
<!--add any projects you want to modify the foo value-->
<SpecialProjectFile Include="**\Project1.csproj" />
</ItemGroup>
<Target Name="Build">
<!--build the most projects and remove the project which you want to change foo property-->
<MSBuild Projects="#(MostProjectFile)" Targets="Build" Properties="Configuration=Debug"/>
<!--build any projects that wants to modify the foo property separately-->
<MSBuild Projects="#(SpecialProjectFile)" Targets="Build" Properties="Configuration=Debug;Foo=42"/>
</Target>
</Project>
2) if you want to change the foo property again, you can just modify the build.proj based on your needs.
Run msbuild xxx\build.proj -t:Build to build it.

MSbuild run specific targets for specific projects

I am using following code in 2 our of 3 projects from the solution default target is set to PublishMe
<Target Name="PublishMe" DependsOnTargets="PipelinePreDeployCopyAllFilesToOneFolder">
<Error Condition="'$(PublishDestination)'==''" Text="The PublishDestination property must be set to the intended publishing destination." />
<MakeDir Condition="!Exists($(PublishDestination))" Directories="$(PublishDestination)" />
<ItemGroup>
<PublishFiles Include="$(_PackageTempDir)\**\*.*" />
</ItemGroup>
<Copy SourceFiles="#(PublishFiles)" DestinationFiles="#(PublishFiles->'$(PublishDestination)\%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="True" />
</Target>
If I run msbuild it builds and publishes just as expected, the problem is that I can't build it from VS anymore, I am having error of missing projectname.dll
Is there way I can change default build target if it is msbuild doing build?
I tried to /t:ProjectName:target;ProjectName2:target, but it isn't working with custom targets for some reason.
Is there way I can specify param from msbuild console to run PublishMe target and if not present run simple build?
You can usefollowing command
msbuild projectname /t:targetname.
Also please check the below link for more info
Building a solution file using msbuild

How to order the harvesting of file before compilation?

I use the _PublishedApplications to generate the structure in the TFS Build Server. After this, I use the <HeatDirectory> in the WiX project to correctly harvest the content of _PublishedApplications folder. But my problem is the order during build.
If I use the <HeatDirectory> inside <Target Name="BeforeBuild"> it doesn't include the binaries copied to the _PublishedApplications, as the harvesting is executed before the publish (file copy).
If I change the target to BeforeCompile the compilation doesn't succeed because there is no file in first place. Here is the code for the WiX project (the relevant part of it):
<ItemGroup>
<Compile Include="Product.wxs" />
<Compile Include="Autogenerated.wxs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SimpleProject\SimpleProject.csproj">
<Name>SimpleProject</Name>
<Project>{GUID}</Project>
<Private>True</Private>
<DoNotHarvest>True</DoNotHarvest>
<RefProjectOutputGroups>Binaries</RefProjectOutputGroups>
<RefTargetDir>INSTALLFOLDER</RefTargetDir>
</ProjectReference>
</ItemGroup>
<Import Project="$(WixTargetsPath)" />
<Target Name="BeforeCompile">
<HeatDirectory OutputFile="Autogenerated.wxs" Directory="$(Sources)"
PreprocessorVariable="var.SimpleProject.TargetDir"
AutogenerateGuids="true" SuppressRegistry="true"
ToolPath="$(WixToolPath)" DirectoryRefId="INSTALLFOLDER"
ComponentGroupName="ComponentGroup_Core"
SuppressRootDirectory="true" />
</Target>
Question
How can I execute the harvesting after the binaries are copied to _PublishedApplications?
I build my solution using the following command:
msbuild SimpleInstaller.sln /p:OutDir=C:\Temp\Output\ /v:diag > C:\Temp\Log.txt
This will output all log to a text file. You can use the MSBuild logger instead.
Then I found when you build a C# project, the target Compile in file Microsoft.Common.targets is invoked. This target has an attribute DependsOnTarget which contains a reference to target BeforeCompile.
I can override this target BeforeCompile in my own C# project, just by adding the following code at the end of it (file .csproj):
...
<Target Name="BeforeCompile">
<!-- custom action. -->
</Target>
</Project>
But the problem is my WiX project cannot override the BeforeCompile target because this target isn't defined for WiX projects. You can check this in the wix2010.targets file. The target Compile only has dependence upon targets PrepareForBuild, ResolveWixExtensionReferences and GenerateCompileWithObjectPath.
My solution was to identify an alternative to BeforeCompile which is the Harvest target. My WiX project (.wixproj) has the following target now:
<Target Name="Harvest">
<HeatDirectory OutputFile="Autogenerated.wxs" Directory="$(Sources)"
PreprocessorVariable="var.SimpleProject.TargetDir"
AutogenerateGuids="true" SuppressRegistry="true"
ToolPath="$(WixToolPath)" DirectoryRefId="INSTALLFOLDER"
ComponentGroupName="ComponentGroup_Core"
SuppressRootDirectory="true" />
</Target>
All this problem occurred because my first project in the solution was the WiX project and only then I added the C# projects. For this reason the BeforeBuild was being executed before everything else.
Another solution to solve this issue is to edit the solution file (.sln) and move the WiX project declaration in the beginning of the solution file to the end of all project declarations (not the end of the solution file). Then the BeforeBuild of the WiX project will be executed after the _PublishedApplications folder is created by the C# project.
This manual edit is required because if you change the Project Build Order you are actually changing the project references (at least in the solution file), but the target BeforeBuild is called anyway before the ResolveProjectReferences which is the responsible for invoking the build of any references.
This is the project declaration that should be after all others:
Project("GUID") = "SimpleInstaller", "SimpleInstaller\SimpleInstaller.wixproj", "GUID"
EndProject
My recommendation is still to use Harvest target as it is independent of any changes in the solution file.
The first thing you need to do is add a project reference from your installer project to your application project. This will force the application to build and be available for your installer project.
You can verify the build sequence of the projects if you right click your solution under VisualStudio and click the "Project Build Order...", if you need to change the order you need to configure the Project dependencies.
Then do something like this in your installer project:
<PropertyGroup>
<RootDir>{PATH TO _PublishedApplications FOLDER}</RootDir>
<HarvestDirectoryNoLogo>true</HarvestDirectoryNoLogo>
<HarvestDirectorySuppressAllWarnings>false</HarvestDirectorySuppressAllWarnings>
<HarvestDirectoryTreatWarningsAsErrors>false</HarvestDirectoryTreatWarningsAsErrors>
<HarvestDirectoryTreatSpecificWarningsAsErrors>
</HarvestDirectoryTreatSpecificWarningsAsErrors>
<HarvestDirectoryVerboseOutput>false</HarvestDirectoryVerboseOutput>
<HarvestDirectoryAutogenerateGuids>false</HarvestDirectoryAutogenerateGuids>
<HarvestDirectoryGenerateGuidsNow>true</HarvestDirectoryGenerateGuidsNow>
<HarvestDirectorySuppressFragments>true</HarvestDirectorySuppressFragments>
<HarvestDirectorySuppressUniqueIds>false</HarvestDirectorySuppressUniqueIds>
</PropertyGroup>
<Import Project="$(WixTargetsPath)" />
<Target Name="BeforeBuild">
<ItemGroup>
<HarvestDirectory Include="$(RootDir)">
<Transforms>
</Transforms>
<ComponentGroupName>MyComponent</ComponentGroupName>
<DirectoryRefId>WebSiteRoot</DirectoryRefId>
<PreprocessorVariable>var.RootDir</PreprocessorVariable>
<SuppressCom>false</SuppressCom>
<SuppressRegistry>false</SuppressRegistry>
<SuppressRootDirectory>true</SuppressRootDirectory>
<KeepEmptyDirectories>true</KeepEmptyDirectories>
</HarvestDirectory>
</ItemGroup>
<HeatDirectory
NoLogo="$(HarvestDirectoryNoLogo)"
SuppressAllWarnings="$(HarvestDirectorySuppressAllWarnings)"
SuppressSpecificWarnings="$(HarvestDirectorySuppressSpecificWarnings)"
ToolPath="$(WixToolPath)"
TreatWarningsAsErrors="$(HarvestDirectoryTreatWarningsAsErrors)"
TreatSpecificWarningsAsErrors="$(HarvestDirectoryTreatSpecificWarningsAsErrors)"
VerboseOutput="$(HarvestDirectoryVerboseOutput)"
AutogenerateGuids="$(HarvestDirectoryAutogenerateGuids)"
GenerateGuidsNow="$(HarvestDirectoryGenerateGuidsNow)"
OutputFile="$(IntermediateOutputPath)_%(HarvestDirectory.Filename)_dir.wxs"
SuppressFragments="$(HarvestDirectorySuppressFragments)"
SuppressUniqueIds="$(HarvestDirectorySuppressUniqueIds)"
Transforms="%(HarvestDirectory.Transforms)"
Directory="#(HarvestDirectory)"
ComponentGroupName="%(HarvestDirectory.ComponentGroupName)"
DirectoryRefId="%(HarvestDirectory.DirectoryRefId)"
KeepEmptyDirectories="%(HarvestDirectory.KeepEmptyDirectories)"
PreprocessorVariable="%(HarvestDirectory.PreprocessorVariable)"
SuppressCom="%(HarvestDirectory.SuppressCom)"
SuppressRootDirectory="%(HarvestDirectory.SuppressRootDirectory)"
SuppressRegistry="%(HarvestDirectory.SuppressRegistry)" />
</Target>
</Project>
I generated this code based on the documentation of the HeatDirectory Task and I use it in real projects.

Creating MSBuild target hooks

Can someone please point me to a reference about target hooks in MSBuild?
I'm looking for something that will let me define targets to run before and after a specified target. I know this can be done using the DependsOnTargets property but I've seen references to using target hooks and I'd like to explore that area.
Thanks,
Zain
A good list of built-in overridable build process hooks can be found here. For custom targets, the only thing I can think of is to use either the DependsOnTarget attribute (like you mentioned) or the BeforeTargets/AfterTargets attribute (like #Ritch Melton mentioned.) Be careful, the BeforeTargets/AfterTargets are only available in MSBuild 4.0
If you understand the idea behind DependsOnTargets then open up the Microsoft.Common.targets file in the .Net SDK directory (C:\Windows\Microsoft.NET\Framework\v3.5). That file defines the build process for the MSBuild task and .Net projects created by Visual Studio. Look for tags called BeforeXXXX, and AfterXXXX. BeforeBuild and AfterBuild are referenced in the default.csproj file - Snippet:
<Import Project="$(MSBuildToolsPath)\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>
-->
</Project>
There are others, like Clean, Rebuild, etc..
Define a Target (or Targets) to execute inside those Target elements, like this (Creates a directory, or list of directories based on the value in the Directories Property:
<Target Name="CreateDir">
<MakeDir Directories="D:\Dogs.txt"/>
</Target>
Then include those Targets in the BeforeXXX Target:
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="BeforeBuild" BeforeTargets="CreateDir">
</Target>
</Project>

Custom common target to build a solution

I created a custom common target "RealClean" which remove every files in the output and "intermediate output" directory. I put it in the Microsoft.Common.targets file.
When I run MsBuild on my csproj everything is fine.
But when I run MsBuild on my sln (which just references a list of csproj) I have the following error
error MSB4057: The target "RealClean" does not exist in the project.
Here is the command line I enter to run MsBuild
C:\Windows\Microsoft .NET\Framework\v3.5\MsBuild.exe /p:Configuration="Release";OutputPath="..\..\MSBuild.Referentiel.net35";nowarn="1591,1573" /t:RealClean mySolution.sln
Any hint?
I had the same issue but didn't want to modify things outside of the source tree in order to get this to work. Adding files to C:\Program Files... means that you have to do this manually on every dev machine to get the same behavior.
I did three things:
1) Created a Custom targets file which I import into every C# and/or VB/F# project in my solution by adding the following to each proj file:
<!-- Rest of project file -->
<PropertyGroup Condition="'$(SolutionDir)' == '' or '$(SolutionDir)' == '*undefined*'">
<!-- Relative path to containing solution folder -->
<SolutionDir>..\</SolutionDir>
</PropertyGroup>
<Import Project="$(SolutionDir)CommonSettings.targets" />
2) Added a clean target which gets called after the real Clean (using the AfterTargets attribute from MSBuild 4.0):
<Target Name="CleanCs" AfterTargets="Clean">
<Message Text="Deep cleaning C# project..." />
<CreateItem Include="$(OutDir)**\*.*; $(ProjectDir)\obj\**\*.*; $(IntermediateOutputPath)**\*.*"
Exclude="**\bin\**\*.vshost.exe; $(IntermediateOutputPath)**\*.log">
<Output TaskParameter="Include" ItemName="AfterClean_FilesToDelete"/>
</CreateItem>
<Delete Files="#(AfterClean_FilesToDelete)" />
<CreateItem Include="$(ProjectDir)\obj\" >
<Output TaskParameter="Include" ItemName="AfterClean_DirectoriesToDelete" />
</CreateItem>
<CreateItem Include ="$(ProjectDir)\bin\" Condition="'$(TargetExt)' != '.exe'" >
<Output TaskParameter="Include" ItemName="AfterClean_DirectoriesToDelete"/>
</CreateItem>
<RemoveDir ContinueOnError="true" Directories="#(AfterClean_DirectoriesToDelete)" />
</Target>
3) In my continuous integration MSBuild project I check and make sure that all proj files have #1:
<ItemGroup>
<!-- Exclude viewer acceptance tests as they must compile as x86 -->
<CheckProjects_CsProjects Include="**\*.csproj" />
</ItemGroup>
<Target Name="CheckProjects">
<!--
Look for C# projects that don't import CommonSettingsCs.targets
-->
<XmlRead XPath="//n:Project[count(n:Import[#Project[contains(string(), 'CommonSettingsCs.targets')]]) = 0]/n:PropertyGroup/n:AssemblyName/text() "
XmlFileName="%(CheckProjects_CsProjects.Identity)"
Namespace="http://schemas.microsoft.com/developer/msbuild/2003"
Prefix="n" >
<Output TaskParameter="Value" ItemName="CheckProjects_CsMissingImports"/>
</XmlRead>
<Error Text="Project missing CommonSettingsCs.targets: %(CheckProjects_CsMissingImports.Identity)"
Condition="'%(CheckProjects_CsMissingImports.Identity)' != ''" />
</Target>
This prevents developers from forgetting to add #1. You could create your own project template to ensure that al new projects have this by default.
The advantage to this approach is setting up a new source tree enlistment doesn't involve anything more than getting the current source tree. The downside is that you have to edit the project files once when you create them.
To work on solution file, MSBuild creates a temporary MSBuild project file containing only some targets like Build and Clean. So you can't call your custom target on a solution file.
Madgnome is probably right. But I wanted to add that you should not be editing the Microsoft.common.targets files. If you do so you risk having a different build process on that machine versus what everybody else has. In your case you could have created a new MSBuild file with just the RealClean target and placed it at
C:\Program Files (x86)\MSBuild\v4.0\Custom.After.Microsoft.Common.targets
or for 32 bit
C:\Program Files\MSBuild\v4.0\Custom.After.Microsoft.Common.targets
and essentially that would be the same as putting that file inside of Microsoft.Common.targets, except you don't have to modify that file.