How do I use my .targets file in Visual Studio with custom build actions? - msbuild

I am a beginner with MSBuild. So far I have been able to create a custom task called 'MakeTextFile' which creates a text file in C:\ based on the contents property you pass it. This works running from a command line prompt.
I have also included this in my .targets file (under the project tag):
<ItemGroup>
<AvailableItemName Include="CreateTextFileAction" />
</ItemGroup>
When I use the Import tag on my client applications .csproj I can now set items build actions to 'CreateTextFileAction', however the action never triggers (as no text file on C:\ is created)
How do I get all the file paths of items that were marked with my build action 'CreateTextFileAction' and pass them onto my custom task?
For reference, my .targets file:
<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<AvailableItemName Include="CreateTextFileAction" />
</ItemGroup>
<UsingTask AssemblyFile="CustomMSBuildTask.dll" TaskName="CustomMSBuildTask.MakeTextFile" />
<Target Name="MyTarget">
<MakeTextFile Contents="TODO HOW DO I GRAB MARKED FILES?" />
</Target>
</Project>

A csproj file has a defined set of targets. The three main entry points are Build, Rebuild and Clean. These targets each have a set of dependencies. If you write your own targets to be part of the standard csproj build you need to find a suitable injection point within these dependencies.
For ease of use there are two standard targets for you to override called BeforeBuild and AfterBuild. If you define this in the csproj file (after the import of the csharp targets file) and call your custom task in there then it should work (or at least move further along).
<Target Name="BeforeBuild">
<MakeTextFile Contents="TODO HOW DO I GRAB MARKED FILES?" />
</Target>

Related

MSBuild - Copy file to output directory if the file isn't in the output directory

I have a nuget with a .targets file that tells the consuming project to copy all files within a "Dependencies" folder to the output directory.
<ItemGroup>
<Files Include="$(MSBuildThisFileDirectory)/../contentFiles/Dependencies/*.*" />
</ItemGroup>
<Target Name="CopyDependencies" AfterTargets="Build">
<Copy SourceFiles="#(Files)"
DestinationFolder="$(TargetDir)" />
</Target>
This nuget is consumed by two projects: Project A and Project B. For this question, let's say we have a System.Runtime.InteropServices.RuntimeInformation.dll that is one of the dependencies within this nuget. The output directory of Project A does not already have System.Runtime.InteropServices.RuntimeInformation.dll, so it gets copied to the output directory when the project is built. Project B however already contains System.Runtime.InteropServices.RuntimeInformation.dll in the output directory. This causes a runtime issue at startup since the targets file is trying to overwrite the existing DLL of the same name with the System.Runtime.InteropServices.RuntimeInformation.dll file from within the nuget (which is a dependency of other files within the output directory).
So, how can I adjust my .targets file to only copy in files that do not already exist within the output directory based on name, and not size or date modified?
There are several ways but probably the most succinct change to your example code would be the following:
<ItemGroup>
<Files Include="$(MSBuildThisFileDirectory)/../contentFiles/Dependencies/*.*" />
</ItemGroup>
<Target Name="CopyDependencies" AfterTargets="Build">
<Copy SourceFiles="#(Files)" DestinationFolder="$(TargetDir)" Condition="!Exists('$(TargetDir)/%(Filename)%(Extension)')" />
</Target>
The change is adding a Condition on the Copy that is using the metadata of the #(Files) collection to test that the file does not exist in $(TargetDir).
Because of the use of metadata, the Copy is a task batch. Essentially the #(Files) collection is divided into batches by %(Filename)%(Extension) and there is a separate Copy task invoked for each batch.
If there is a large number of files in the Dependencies folder, the following variant may provide better performance.
<ItemGroup>
<Files Include="$(MSBuildThisFileDirectory)/../contentFiles/Dependencies/*.*" />
</ItemGroup>
<Target Name="CopyDependencies" AfterTargets="Build">
<ItemGroup>
<FilesToCopy Include="#(Files)" Condition="!Exists('$(TargetDir)/%(Filename)%(Extension)')" />
</ItemGroup>
<Copy SourceFiles="#(FilesToCopy)" DestinationFolder="$(TargetDir)" />
</Target>
The task batching is moved to the definition of a new ItemGroup collection and the Copy task is invoked once for the set of files. The potential performance improvement is that the implementation of the Copy task tries to parallelize copies, which it can't do when invoked per file.

Call MSBuild passing it a single task to run

We can define a target in a csproj file and then specify that target when we call msbuild from the command line. That looks like this:
my.csproj
<Target Name="CopyFiles">
<Copy
SourceFiles="#(MySourceFiles)"
DestinationFolder="c:\MyProject\Destination" />
</Target>
msbuild
msbuild my.csproj /t:CopyFiles
The CopyFiles targets asks msbuild to run the Copy task.
What if we don't want to edit the csproj file. How can we define a target just from the command line? Alternatively, using only the command line, how can we ask msbuild to run just one or maybe two tasks?
Pseudo-Code
msbuild my.csproj /t:"Copy SourceFiles=#(MySourceFiles) DestinationFolder=..."
Based on the MSBuild Command-Line Reference, this isn't possible exactly as you describe it, i.e. MSBuild will not take Target definitions from the command-line input. They have to be defined in a file somewhere.
But, you can have a script (maybe even a .bat?) that does something like:
Create a new, essentially empty, project file.
Import the .csproj, with <Import Project="foo.csproj" />
Add the targets
Call MSBuild with the new project file with the targets you want. Multiple targets can be specified by multiple /t: switches. Properties can be specified with /p:
The final, programmatically/script generated project file might look something like:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="foo.csproj" />
<Target Name="CopyFiles">
<Copy
SourceFiles="#(MySourceFiles)"
DestinationFolder="$(Destination)" />
</Target>
</Project>
The actual MSBuild command called might then be:
msbuild temp.proj /t:CopyFiles /p:"Destination=c:\MyProject\Destination"

custom .targets file not working

I have created a custom .targets file as below (Just added all the common tasks required in myproj.vcxproj file to .targets file)
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- *******************************************************************************************
Common tasks
******************************************************************************************* -->
<Target Name="H1">
<Exec Command="del /F/Q #(S_PACK_H1)" />
<RemoveDir Directories="#(D_PACK_H1)" />
</Target>
<Target Name="H2">
<Exec Command="del /F/Q #(S_PACK_H2)" />
<RemoveDir Directories="#(D_PACK_H2)" />
</Target>
<Target Name="H11">
<Exec Command="del /F/Q #(S_PACK_H11)" />
<RemoveDir Directories="#(D_PACK_H11)" />
</Target>
</Project>
All the macros/arrays like S_PACK_H1, D_PACK_H11 are defined in myproj.vcxproj file after which I am importing this in myproj.vcxproj file as below
<Import Project="C:\Program Files\MSBuild\MyCompany\Mycustom.targets" />
when I use the below cmd
msbuild myproj.vcxproj /t:H11
it gives an error "error MSB4057: The target "H11" does not exist in the project"
but If I have the same list of tasks in .vcxproj file instead of .targets file then it works fine.
Can I define macros in .vcxproj file and use them in .targets file? Will MSBuild be able to get that definition/value? If not then how do I go about using/passing something defined in vxcproj file in .targets file?
Why is msbuild not able to see my task when it is in .targets file Vs .proj file? what else do I need to do?
There is no obvious reason for this not to work. Yes you can define targets in an imported file and they should be available, regardless of where the import occurs. If you are using MSBuild 4.0 (there is no ToolsVersion attribute on your .targets file above, so I'm not sure) then you can generate a fully preprocessed file, like this:
> msbuild mproj.vcxproj /pp
Look for the preprocessed file in the same folder. Open it up in a text editor and search for your imported content, it should all be there. If not, perhaps the preprocessed file can shed some light into what is going wrong.

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>

Replace .sln with MSBuild and wrap contained projects into targets

I'd like to create a MSBuild project that reflects the project dependencies in a solution and wraps the VS projects inside reusable targets.
The problem I like solve doing this is to svn-export, build and deploy a specific assembly (and its dependencies) in an BizTalk application.
My question is: How can I make the targets for svn-exporting, building and deploying reusable and also reuse the wrapped projects when they are built for different dependencies?
I know it would be simpler to just build the solution and deploy only the assemblies needed but I'd like to reuse the targets as much as possible.
The parts
The project I like to deploy
<Project DefaultTargets="Deploy" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ExportRoot Condition="'$(Export)'==''">Export</ExportRoot>
</PropertyGroup>
<Target Name="Clean_Export">
<RemoveDir Directories="$(ExportRoot)\My.Project.Dir" />
</Target>
<Target Name="Export_MyProject">
<Exec Command="svn export svn://xxx/trunk/Biztalk2009/MyProject.btproj --force" WorkingDirectory="$(ExportRoot)" />
</Target>
<Target Name="Build_MyProject" DependsOnTargets="Export_MyProject">
<MSBuild Projects="$(ExportRoot)\My.Project.Dir\MyProject.btproj" Targets="Build" Properties="Configuration=Release"></MSBuild>
</Target>
<Target Name="Deploy_MyProject" DependsOnTargets="Build_MyProject">
<Exec Command="BTSTask AddResource -ApplicationName:CORE -Source:MyProject.dll" />
</Target>
</Project>
The projects it depends upon look almost exactly like this (other .btproj and .csproj).
Wow, this is a loaded question for a forum post. I wrote about 20 pages on creating reusable .targets files in my book, but I'll get you started here with the basics here. I believe that the key to creating reusable build scripts (i.e. .targets files) is three elements:
Place behavior (i.e. targets) into separate files
Place data (i.e. properties and items, these are called .proj files) into their own files
Extensibility
.targets files should validate assumptions
The idea is that you want to place all of your targets into separate files and then these files will be imported by the files which will be driving the build process. These are the files which contain the data. Since you import the .targets files you get all the targets as if they had been defined inline. There will be a silent contract between the .proj and .targets files. This contract is defined in properties and items which both use. This is what needs to be validated.
The idea here is not new. This pattern is followed by .csproj (and other projects generated by Visual Studio). If you take a look your .csproj file you will not find a single target, just properties and items. Then towards the bottom of the file it imports Microsoft.csharp.targets (may differ depending on project type). This project file (along with others that it imports) contains all the targets which actually perform the build.
So it's layed out like this:
SharedBuild.targets
MyProduct.proj
Where MyProdcut.proj might look like:
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- This uses a .targets file to off load performing the build -->
<PropertyGroup>
<Configuration Condition=" '$(Configuration)'=='' ">Release</Configuration>
<OutputPath Condition=" '$(OutputPath)'=='' ">$(MSBuildProjectDirectory)\BuildArtifacts\bin\</OutputPath>
</PropertyGroup>
<ItemGroup>
<Projects Include="$(MSBuildProjectDirectory)\..\ClassLibrary1\ClassLibrary1.csproj"/>
<Projects Include="$(MSBuildProjectDirectory)\..\ClassLibrary2\ClassLibrary2.csproj"/>
<Projects Include="$(MSBuildProjectDirectory)\..\ClassLibrary3\ClassLibrary3.csproj"/>
<Projects Include="$(MSBuildProjectDirectory)\..\WindowsFormsApplication1\WindowsFormsApplication1.csproj"/>
</ItemGroup>
<Import Project="SharedBuild.targets"/>
</Project>
And SharedBuild.targets might look like:
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- This represents a re-usable build file -->
<Target Name="SharedBuild_Validate">
<!-- See http://sedodream.com/2009/06/30/ElementsOfReusableMSBuildScriptsValidation.aspx for more info
about this validation pattern
-->
<ItemGroup>
<_RequiredProperties Include ="Configuration">
<Value>$(Configuration)</Value>
</_RequiredProperties>
<_RequiredProperties Include ="OutputPath">
<Value>$(OutputPath)</Value>
</_RequiredProperties>
<_RequiredItems Include="Projects">
<RequiredValue>%(Projects.Identity)</RequiredValue>
<RequiredFilePath>%(Projects.Identity)</RequiredFilePath>
</_RequiredItems>
</ItemGroup>
<!-- Raise an error if any value in _RequiredProperties is missing -->
<Error Condition="'%(_RequiredProperties.Value)'==''"
Text="Missing required property [%(_RequiredProperties.Identity)]"/>
<!-- Raise an error if any value in _RequiredItems is empty -->
<Error Condition="'%(_RequiredItems.RequiredValue)'==''"
Text="Missing required item value [%(_RequiredItems.Identity)]" />
<!-- Validate any file/directory that should exist -->
<Error Condition="'%(_RequiredItems.RequiredFilePath)' != '' and !Exists('%(_RequiredItems.RequiredFilePath)')"
Text="Unable to find expeceted path [%(_RequiredItems.RequiredFilePath)] on item [%(_RequiredItems.Identity)]" />
</Target>
<PropertyGroup>
<BuildDependsOn>
SharedBuild_Validate;
BeforeBuild;
CoreBuild;
AfterBuild;
</BuildDependsOn>
</PropertyGroup>
<Target Name="Build" DependsOnTargets="$(BuildDependsOn)"/>
<Target Name="BeforeBuild"/>
<Target Name="AfterBuild"/>
<Target Name="CoreBuild">
<!-- Make sure output folder exists -->
<PropertyGroup>
<_FullOutputPath>$(OutputPath)$(Configuration)\</_FullOutputPath>
</PropertyGroup>
<MakeDir Directories="$(_FullOutputPath)"/>
<MSBuild Projects="#(Projects)"
BuildInParallel="true"
Properties="OutputPath=$(_FullOutputPath)"/>
</Target>
</Project>
Don't look too much at the SharedBuild_Validate target yet. I put that there for completeness but don't focus on it. You can find more info on that at my blog at http://sedodream.com/2009/06/30/ElementsOfReusableMSBuildScriptsValidation.aspx.
The important parts to notice are the extensibility points. Even though this is a very basic file, it has all the components of a reusable .targets file. You can customize it's behavior by passing in different properties and items to build. You can extend it's behavior by overriding a target (BeforeBuild, AfterBuild or even CoreBuild) and you can inject your own targets into the build with:
<Project ...>
...
<Import Project="SharedBuild.targets"/>
<PropertyGroup>
<BuildDependsOn>
$(BuildDependsOn);
CustomAfterBuild
</BuildDependsOn>
</PropertyGroup>
<Target Name="CustomAfterBuild">
<!-- Insert stuff here -->
</Target>
</Project>
In your case I would create an SvnExport.targets file which uses the required properties:
SvnExportRoot
SvnUrl
SvnWorkingDirectory
You will use these properties to do the Export.
Then create another one for Biztalk build and deploy. You could split this up into 2 if necessary.
Then inside of your .proj file you just import both and setup the targets to build in the right order, and your off.
This is only really the beginning of creating reusable build elements, but this should get the wheels turning in your head. I am going to post all of this to my blog as well as download links for all files.
UPDATE:
Posted to blog at http://sedodream.com/2010/03/19/ReplacingSolutionFilesWithMSBuildFiles.aspx