Incremental Build of Nuget Packages - msbuild

I want to execute an msbuild project which uses batching to determine that one or more csproj projects have been freshly-built, and therefore require fresh nuget packaging. The script I've made so far seems like a reasonable start, but it the incremental-build mechanism isn't working. The MainBuild target executes every time, no matter what.
Here is what I have:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="MainBuild" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)'=='' ">Debug</Configuration>
<Content>content\plugins\</Content>
</PropertyGroup>
<ItemGroup>
<Nuspec Include="$(MSBuildProjectDirectory)\plugins\*\*.nuspec" />
</ItemGroup>
<Target Name="MainBuild"
Inputs="%(Nuspec.RootDir)%(Nuspec.Directory)bin\$(Configuration)\*.dll"
Outputs="%(Nuspec.RootDir)%(Nuspec.Directory)%(FileName).pkg" >
<ItemGroup>
<Inputs Include="%(Nuspec.RootDir)%(Nuspec.Directory)bin\$(Configuration)\*.dll" />
<Outputs Include="%(Nuspec.RootDir)%(Nuspec.Directory)%(FileName).pkg" />
</ItemGroup>
<Message Text="INPUTS: %(Inputs.FullPath)" />
<Message Text="OUTPUTS: #(Outputs->'%(FullPath)')" />
<Copy SourceFiles="#(Inputs)" DestinationFiles="#(Outputs->'%(FullPath)')" />
</Target>
</Project>
The Copy task is just a debugging placeholder for calling-out to nuget and creating a new package.
The idea is that if any files in the bin\Debug directory are newer than the corresponding .nuspec file (found two folders above bin\Debug), then the MainBuild target should execute.
Any ideas?
p.s. The Inputs and Outputs attributes of the Target presumably each create an item. I think it strange that the items created can't be referenced inside the target. In the above example, I had to make a target-interna dynamic ItemGroup to re-create the items, just so that I could access them. Is there a way around that?

I read this in the MSBuild Batching documentation
If a task inside of a target uses batching, MSBuild needs to determine
if the inputs and outputs for each batch of items is up-to-date.
Otherwise, the target is executed every time it is hit.
Which may be the cuprit. Try changing your copy target to use batching instead of an ite transform (I don't think using item metadata in an item group satisfies the above requirement).
<Target Name="MainBuild"
Inputs="%(Nuspec.RootDir)%(Nuspec.Directory)bin\$(Configuration)\*.dll"
Outputs="%(Nuspec.RootDir)%(Nuspec.Directory)%(FileName).pkg" >
<ItemGroup>
<Inputs Include="%(Nuspec.RootDir)%(Nuspec.Directory)bin\$(Configuration)\*.dll" />
<Outputs Include="%(Nuspec.RootDir)%(Nuspec.Directory)%(FileName).pkg" />
</ItemGroup>
<Message Text="INPUTS: %(Inputs.FullPath)" />
<Message Text="OUTPUTS: #(Outputs->'%(FullPath)')" />
<Copy SourceFiles="#(Inputs)" DestinationFiles="%(Outputs.FullPath)" />
</Target>
It looks like the number of inputs may be different than the number of outputs (I suspect there is more than one .dll files in the output directory for each project), which will also cause the target to execute.

Related

Deploying from MSBuild without overwriting specific files

So here's what I want to do:
I want a build script that will xcopy deploy build outputs for a legacy winform app to a given directory. I want to specify a list of files to not overwrite (some config files).
I would rather have the list of files to not overwrite be passed as a parameter than hard code them.
This seems to be really unexpectedly hard. Here's what I have so far:
<!-- A property that is passed a semicolon delimited list of file names -->
<PropertyGroup>
<ProtectedFiles/>
</PropertyGroup>
<--! An ItemGroup to pick up the files>
<ItemGroup>
<FilesToDelete Include=$(DeploymentTargetFolder)\*.* Exclude="#(ProtectedFiles->'$(DeployTargetFolder)\%(identity)')"
<ItemGroup/>
<--! the delete isn't working, so I will stop just with that to keep the code brief -->
<Delete Files="#(FilesToDelete)"/>
The delete just ignores the exclude files and deletes everything
Is there a better way to do this? It doesn't seem too crazy -- I just want to
Delete all files from the target directory, except for the config files
Copy all of the files from the build outputs to the target directory, without overwriting the config files.
The first problem with your particular markup appears to confuse MsBuild $(properties) with MsBuild %(items) and MsBuild #(itemgroups).
ProtectedFiles is a property:
<!-- A property that is passed a semicolon delimited list of file names -->
<PropertyGroup>
<ProtectedFiles/>
</PropertyGroup>
But it's being treated as an Item and wouldn't have any %item.metadata:
<--! An ItemGroup to pick up the files>
<ItemGroup>
<FilesToDelete Include=$(DeploymentTargetFolder)\*.* Exclude="#(ProtectedFiles->'$(DeployTargetFolder)\%(identity)')"
<ItemGroup/>
Save the following markup locally as "foo.xml", then call "msbuild.exe foo.xml" and observe the output:
<Project ToolsVersion="4.0" DefaultTargets="foo" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<FilesProp>FileA.txt;FileB.txt</FilesProp>
</PropertyGroup>
<ItemGroup>
<ProtectedFiles Include="FileA.txt" />
<ProtectedFiles Include="FileA.txt" />
</ItemGroup>
<Target Name="foo">
<Message Importance="high" Text="ProtectedFiles ItemGroup: #(ProtectedFiles)" />
<Message Importance="high" Text="ProtectedFiles ItemGroup transform: #(ProtectedFiles->'%(Identity)')" />
<Message Importance="high" Text="FilesProp Property: $(FilesProp)" />
<Message Importance="high" Text="FilesProp Property: #(FilesProp->'%(FilesProp.Identity)')" />
</Target>
</Project>
Will yield the following output:
foo:
ProtectedFiles ItemGroup: FileA.txt;FileA.txt
ProtectedFiles ItemGroup transform: FileA.txt;FileA.txt
FilesProp Property: FileA.txt;FileB.txt
FilesProp Property:
If you're unable to change the design and need to convert a Property comprising a semi-colon delimited list of file paths, use the MsBuild <CreateItem /> task.
Add this markup to foo.xml occurring after the Foo target, then invoke msbuild again, but using the "bar" target (e.g. msbuild.exe foo.xml /t:bar)
<Target Name="bar">
<CreateItem Include="$(FilesProp)">
<Output TaskParameter="Include" ItemName="TheFiles"/>
</CreateItem>
<Message Text="TheFiles ItemGroup: #(TheFiles)" Importance="high" />
<Message Text="Output each item: %(TheFiles.Identity)" Importance="high" />
</Target>
Will yield the following output:
bar:
TheFiles ItemGroup: FileA.txt;FileB.txt
Output each item: FileA.txt
Output each item: FileB.txt
Next you should rethink some of your assumptions. I don't believe the file extension should be the determining factor when deciding which files to update, rather you should rely on MsBuild's ability to build tasks incrementally allowing it to perform a task only if the inputs are newer than the outputs. You can do this by using an MsBuild <Copy /> task configured to skip unchanged files.
Add this markup to the above Xml file, then modify the $(SourceFolder) and $(TargetFolder) to point to a source folder you'd like to copy recursively, and a destination folder to place the files. Build using "msbuild.exe foo.xml /t:Deployment" and observe the output.
<Target Name="Deployment">
<PropertyGroup>
<SourceFolder>c:\sourcefolder\</SourceFolder>
<TargetFolder>c:\destinationfolder\</TargetFolder>
</PropertyGroup>
<CreateItem Include="$(SourceFolder)\**\*.*">
<Output TaskParameter="Include" ItemName="FilesToCopy" />
</CreateItem>
<Copy SourceFiles="#(FilesToCopy)" DestinationFolder="$(TargetFolder)%(RecursiveDir)" SkipUnchangedFiles="true" />
</Target>
Without modifying any of the source files, run the command again and note that no files were copied.
Modify a file in the source folder, then run the command again. Notice that only the updated files were copied?
I hope this gets you on the right track.
There seems to be an already existing post, similar to this. Please check this Trying to exclude certain extensions doing a recursive copy (MSBuild)

Issue with using MSBuild to build and copy all outputs to a common folder

We are trying to write a msbuild script that will build the solution and copy over all the compiled binaries and dependencies over to a specific output folder. While the build script that we have does build and copy over the binaries to a common folder, but we are not getting the dependencies copied.
This probably has to do with the way we have used the msbuild task to build the solution and we are accepting the targetoutputs of the task into an itemgroup and iterating over the item group to copy all the compiled dlls and exes over to a common folder. But this is not including the dependency dlls which gets placed into the individual bin folder of each project.
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ParentSolutionFile />
</PropertyGroup>
<ItemGroup>
<Assemblies Include="*.dll, *.exe" />
</ItemGroup>
<Target Name="BuildAll">
<CombinePath BasePath="$(MSBuildProjectDirectory)" Paths="Source\Solutions\xxx.sln">
<Output TaskParameter="CombinedPaths" PropertyName="ParentSolutionFile" />
</CombinePath>
<Message Text="$(ParentSolutionFile)" />
<MSBuild Projects="$(ParentSolutionFile)">
<Output TaskParameter="TargetOutputs" ItemName="Assemblies" />
</MSBuild>
<Message Text="%(Assemblies.Identity)" />
<Copy SourceFiles="%(Assemblies.Identity)" DestinationFolder="$(MSBuildProjectDirectory)\Binary" OverwriteReadOnlyFiles="True" SkipUnchangedFiles="True" />
</Target>
What will be the preferred way to copy over all the binaries along with the necessary dependencies to a common output folder?
Does not overriding OutputPath do the trick alone?
<MSBuild Projects="$(ParentSolutionFile)" Properties="OutputPath=$(MSBuildProjectDirectory)\Binary">
<Output TaskParameter="TargetOutputs" ItemName="Assemblies" />
</MSBuild>
And leave out the copy task alltogether?
The build process will place the final result in the directory represented by OutputPath - at least if you are building c# projects. For C/C++ the internal structure and variable names are completely different.
Thus, in theory, you could pass the OutputPath in the MsBuild-task that builds the solution.
<MsBuild Projects="$(ParentSolutionFile)"
Properties="OutputPath=$(MSBuildProjectDirectory)\Binary"/>
However, the csproj-files will overwrite that value unconditionally with the following code:
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\Debug\</OutputPath>
I have solved this by injecting my own build system in each and every csproj-file.
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\..\..\build\buildsystem.targets" />
The path is relative to the csproj-file. An absolute path is fine too, or a variable. The trick is to make it work on all dev machines as well as the build agents.
Now, in buildsystem.targets, simply redefine OutputPath as much as you like. Again, the trick is to ensure you get the same - or at least a well defined - location regardless of who builds it (dev, build agent) and regardless how the build was initiated (VS, command line).
A simple way of handling the differences is to import conditionally.
<Import Project="..\..\..\build\buildsystem.targets"
Condition="'$(BuildingInsideVisualStudio)'!='true'"/>
That will give you no changes if initiating the build from VS and whatever changes you code for if you build from command line.
--Jesper

Filter Global MSBuild Items

Starting with a .csproj which defines various xml Content files.
Have code generation Target which takes some xml files (Target Inputs) and generate .cs files whose names are determined by transformation from the xml files (Target Outputs).
In order for MSBuild to determine whether the code building Target needs to run, it needs to inspect the Target Inputs and Outputs. Therefore I am assuming that those Target Inputs and Outputs must be global.
If that's incorrect, there should be another question about how to create a Target who's Outputs are based on Dynamic Items; tried it but the Target keeps being called.
If it's correct, then how to filter the Content at the global level ?
Specifically, I want to filter Content Items in the project so that only the one's in a specific directory are used. The Content Items will be added by other developers via the IDE.
This can be achieved using a Target which creates Dynamic Items, doing the filtering in the Condition attribute. That requires Target Batching, which isn't available globally. Using MSBuild 3.5 and Visual Studio 2008.
<?xml version="1.0" encoding="utf-8"?>
<Project
DefaultTargets="Show" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Content Include="badxml\somebadxml1.xml" />
<!-- Note xml\somexml2.xml exists on disk, it just isn't used in this project. -->
<Content Include="xml\somexml1.xml" />
<Content Include="xml\somexml3.xml" />
</ItemGroup>
<!-- Foo should only be defined for Content Items in the "xml" directory. -->
<ItemGroup>
<Foo Include="#(Content->'%(Filename)')"/>
<!-- The line below doesn't work -->
<!-- TestFilter.proj(10,10): error MSB4090: Found an unexpected character '%' at position 3 in condition " '%(Content.RelativeDir)'=='xml' ". -->
<!-- <Foo Condition=" '%(Content.RelativeDir)'=='xml' " Include="#(Content->'%(Filename)')"/> -->
</ItemGroup>
<Target Name="ShowContent">
<Message Text="Content: %(Content.Identity)" />
<Message Text="Content RelDir: %(Content.RelativeDir)" />
</Target>
<Target Name="ShowFoo">
<Message Text="Foo: %(Foo.Identity)" />
</Target>
<Target Name="Show">
<CallTarget Targets="ShowContent;ShowFoo" />
</Target>
</Project>
Why doesn't MSBuild ItemGroup conditional work in a global scope addresses the same issue but from the perspective of asking why this doesn't work, rather than looking for alternative approaches.
Filtering Item's Metadata in msbuild uses Dynamic Items in a Target, and a dummy Output name.
My best guess is that this can't be done without using Dynamic Items in a Target, and the workaround will be rather than using Items which require a Condition, to write out a file with a predefined name and use that as Output placeholder.
So it turns out my assumption was incorrect. It's perfectly acceptable to have Target Outputs which are based on dynamic items. It helps to remember that Targets are batched according to the Outputs definition.
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="TestBatch"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Static Item declaration. -->
<ItemGroup>
<Bar Include="Static01">
<Data>Static01 Data</Data>
</Bar>
</ItemGroup>
<Target Name="PreBatchTarget">
<!-- Dynamic Item addition. -->
<ItemGroup>
<Bar Include="Dynamic01">
<Data>Dynamic01 Data</Data>
</Bar>
</ItemGroup>
</Target>
<Target Name="TestBatchTarget"
Outputs="%(Bar.Data)"
>
<Message Text="TestBatchTarget call" />
<Message Text="#(Bar)" />
</Target>
<Target Name="TestBatch"
DependsOnTargets="PreBatchTarget;TestBatchTarget"
>
</Target>
</Project>
msbuild /nologo DynamicTargetOutput.proj
Project "DynamicTargetOutput.proj" on node 0 (default targets).
TestBatchTarget call
Static01
TestBatchTarget:
TestBatchTarget call
Dynamic01
Done Building Project "DynamicTargetOutput.proj" (default targets).
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:00.09

Additional paths in msbuild script

How to specify additional assembly reference paths for the MSBuild tasks?
I have following script so far, but can't figure out how to specify additional search paths.
<ItemGroup>
<ProjectsToBuild Include="..\Main\Main.sln" />
</ItemGroup>
<!-- The follwing paths should be added to reference search paths for the build tasks -->
<ItemGroup>
<MyAddRefPath Include="$(MSBuildProjectDirectory)\..\..\Build\Lib1" />
<MyAddRefPath Include="$(MSBuildProjectDirectory)\..\..\Build\Lib2" />
</ItemGroup>
<MSBuild
Projects="#(ProjectsToBuild)"
Properties="Configuration=Debug;OutputPath=$(BuildOutputPath)">
</MSBuild>
UPDATE:
Please show one complete working script which invokes original project, such as an SLN with multiple additional reference paths.
No suggestions on how to improve the project structure please.
I know how to build a good structure, but now it's the task of building an existing piece of crap.
I have finaly figured out how to do it:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ProjectsToBuild Include="ConsoleApplication1\ConsoleApplication1.csproj" />
</ItemGroup>
<ItemGroup>
<AdditionalReferencePaths Include="..\Build\ClassLibrary1" />
<AdditionalReferencePaths Include="..\Build\ClassLibrary2" />
</ItemGroup>
<PropertyGroup>
<BuildOutputPath>..\Build\ConsoleApplication1</BuildOutputPath>
</PropertyGroup>
<Target Name="MainBuild">
<PropertyGroup>
<AdditionalReferencePathsProp>#(AdditionalReferencePaths)</AdditionalReferencePathsProp>
</PropertyGroup>
<MSBuild
Projects="ConsoleApplication1\ConsoleApplication1.csproj"
Properties="ReferencePath=$(AdditionalReferencePathsProp);OutputPath=$(BuildOutputPath)"
>
</MSBuild>
</Target>
The property you want to modify is AssemblySearchPaths. See the ResolveAssemblyReference task more information.
<Target Name="AddToSearchPaths">
<CreateProperty Value="x:\path\to\assemblies;$(AssemblySearchPaths)">
<Output PropertyName="AssemblySearchPaths" TaskParameter="Value" />
</CreateProperty>
</Target>
Making use of item groups, as in your example, it would look like:
<Target Name="AddToSearchPaths">
<CreateProperty Value="#(MyAddRefPath);$(AssemblySearchPaths)">
<Output PropertyName="AssemblySearchPaths" TaskParameter="Value" />
</CreateProperty>
</Target>
Looking in %WINDIR%\Microsoft.NET\Framework\v2.0.50727\Microsoft.Common.targets, you can see that the ResolveAssemblyReference Task is executed as part of the ResolveAssemblyReferences target. Thus, you want the newly added target to modify the AssemblySearchPaths property before ResolveAssemblyReferences is executed.
You've stated that you want to be able to modify the assembly search paths without modifying the project files directly. In order to accomplish that requirement you need to set an environment variable that will override the AssemblySearchPaths. With this technique you will need to provide every assembly reference path used by all the projects in the solutions. (Modifying the projects or copies of the projects would be easier. See final comments.)
One technique is to create a batch file that runs your script at sets the environment variable:
set AssemblySearchPaths="C:\Tacos;C:\Burritos;C:\Chalupas"
msbuild whatever.msbuild
Another way is to define a PropertyGroup in your custom msbuild file (otherwise known as the "hook" needed to make this work):
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ProjectsToBuild Include="..\Main\Main.sln" />
</ItemGroup>
<PropertyGroup>
<AssemblySearchPaths>$(MSBuildProjectDirectory)\..\..\Build\Lib1;$(MSBuildProjectDirectory)\..\..\Build\Lib2</AssemblySearchPaths>
</PropertyGroup>
<Target Name="Build">
<MSBuild Projects="#(ProjectsToBuild)" Properties="AssemblySearchPaths=$(AssemblySearchPaths);Configuration=Debug;OutputPath=$(OutputPath)" />
</Target>
</Project>
Now if it were me, and for whatever unexplained reason I couldn't modify the project files to include the updated references that I am going to build with, I would make copies of the project files, load them into the IDE, and correct the references in my copies. Synching the projects becomes a simple diff/merge operation which is automatic with modern tools like mercurial (heck I'm sure clearcase could manage it too).
...and remember that you don't need to use a target for this, you can use project-scoped properties or items, as...
<ItemGroup>
<MyAddRefPath Include="$(MSBuildProjectDirectory)\..\..\Build\Lib1" />
<MyAddRefPath Include="$(MSBuildProjectDirectory)\..\..\Build\Lib2" />
</ItemGroup>
<PropertyGroup>
<MyAddRefPath>$(MSBuildProjectDirectory)\..\..\Build\Lib3</MyAddRefPath>
<!-- add in the property path -->
<AssemblySearchPaths>$(MyAddRefPath);$(AssemblySearchPaths)</AssemblySearchPaths>
<!-- add in the item paths -->
<AssemblySearchPaths>#(MyAddRefPath);$(AssemblySearchPaths)</AssemblySearchPaths>
</PropertyGroup>
...and if you do need to do this in a target to pick up paths from a dynamically populated item group, use inline properties, not the CreateProperty task (if you are not stuck in v2.0)
<Target Name="AddToSearchPaths">
<PropertyGroup>
<!-- add in the item paths -->
<AssemblySearchPaths>#(MyDynamicAddRefPath);$(AssemblySearchPaths)</AssemblySearchPaths>
</PropertyGroup>
</Target>

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