Make VisualStudio run a command to generate C# code - c#

I'm trying to do something extremely simple, yet despite hours of scouring the Internet and desperately typing in every tiny example fragment I can find, I literally cannot make VS do what I want. Seriously frustrated here!
This is what I want:
When I do a build, I want VS to find every *.fubar file in the project.
For each *.fubar file, I want VS to execute fubar.exe with the input file as argument.
I want VS to compile the resulting *.cs files as usual.
I am damn-well convinced it must be possible to achieve this trivially simple task. I just need to figure out how.
So that's the problem. Let me explain what I've researched so far.
It seems that you can write a VS extension by implementing some COM interface. But that requires you to edit the Registry to tell VS where the plugin is. Obviously that's completely unacceptable; I should not have to reconfigure the OS just to run a command-line tool!
It appears that MSBuild is supposed to be this ultra-configurable build management tool where you can define an arbitrary set of build steps and their interdependencies. It seems like it should be trivial to add an extra task before the build C# compilation step that runs fubar.exe to generate the source code. And yet, after hours of trying, I cannot get a single C# file to appear.
Some documents talk about special pre-build and post-build steps. But it seems silly to need a special hook when the number and order of build steps is supposed to be arbitrary to begin with. Regardless, I tried it, and it still didn't work.
To be clear: Playing around with the project file makes VS display the item properties slightly differently, and doesn't make the build fail. But it also doesn't ever make any C# files appear. (At least, I can't find them anywhere on the disk. I can't tell if my tool is actually being run or not — I suspect "not".)
Can anybody tell me how to make this trivial, trivial thing work? There must be a way!

I agree that T4 may be a prettier solution, but the below MsBuild code works.
You can create a custom targets file that does the calling, I created one that simply copies files, but the process should be similar for invoking your executable.
Make sure you set your FuBar files to BuildAction "Fubars". The Build action has been defined in the targets file using the "AvailableItemName" property.
You may need to extend the below script with a couple of Conditions and Exists checks to make sure it runs when needed. I called it convert.targets and places it in the project directory.
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Ensure Visual Studio puts the FuBars item in the Build Action dropdown -->
<ItemGroup>
<AvailableItemName Include="FuBars" />
</ItemGroup>
<!-- Execute action on all items matching FuBar (only if output is missing or input has changed) -->
<Target Name="GenerateCodeFubar" Inputs="#(Fubars)" Outputs="$(MSBuildProjectDirectory)/obj/$(Configuration)/Fubars/%(Fubars.Identity).g.cs" >
<MakeDir Directories="$(MSBuildProjectDirectory)/obj/$(Configuration)/Fubars/%(Fubars.RelativeDir)" />
<Exec Command="cmd /c copy "%(FuBars.FullPath)" "$(MSBuildProjectDirectory)/obj/$(Configuration)/Fubars/%(FuBars.Identity).g.cs""/>
</Target>
<!-- Pick up generated code in the Compile group. Separated from the execute action so that the previous action only runs when files have changes -->
<Target Name="IncludeCodeFubarInCompile">
<ItemGroup>
<GeneratedFubars Include="$(MSBuildProjectDirectory)/obj/$(Configuration)/Fubars/%(FuBars.Identity).g.cs"/>
</ItemGroup>
<CreateItem Include="%(GeneratedFubars.FullPath)">
<Output TaskParameter="Include" ItemName="Compile" />
</CreateItem>
</Target>
<!-- Clean up for Rebuild or Clean -->
<Target Name="DeleteCodeFubar">
<RemoveDir Directories="$(MSBuildProjectDirectory)/obj/$(Configuration)/Fubars/" />
<Delete Files="$(MSBuildProjectDirectory)/obj/$(Configuration)/fubars/**/*.g.cs" />
</Target>
<!-- Instruct MsBuild when to call target on Build-->
<PropertyGroup>
<BuildDependsOn>
GenerateCodeFubar;
IncludeCodeFubarInCompile;
$(BuildDependsOn);
</BuildDependsOn>
</PropertyGroup>
<!-- Instruct MsBuild when to call target on Clean-->
<PropertyGroup>
<CleanDependsOn>
DeleteCodeFubar;
$(CleanDependsOn);
</CleanDependsOn>
</PropertyGroup>
</Project>
Include it in your .csproj after the import the last targets file:
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- IMPORT HERE -->
<Import Project="convert.targets" />
<!-- END: IMPORT HERE -->
<!-- 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>
You may have to tell Visual Studio to turn off the Host Compiler, as Visual Studio caches the contents of the <Compile> group for performance. Though a quick test shows this may not be needed.
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<!-- The first property group should not have any condition on it -->
<PropertyGroup>
<!-- Include below item in the first PropertyGroup item:
<UseHostCompilerIfAvailable>FALSE</UseHostCompilerIfAvailable>
</PropertyGroup>

This is extremely easy.
In your project properties select the Build Events tab and enter a correct Pre-build event command line.
Without knowing what footer.exe is nor its command line args, I can't give you any specific help for that app. But there is a "Macros" button on the pre-build event dialog that can help you with various substitution variables.
Additionally, VS comes with the T4 templating system which might be a better fit than whatever footer.exe is.

Related

How does Visual Studio determine whether it has to start MSBuild or not?

I've done a lot of MSBuild customization for my C++ projects in the past. The Input and Output attributes of a MSBuild target are used to determine whether the target has to be executed or not. Additionally Visual Studio uses the .tlog files (located in the intermediate directory) to determine whether MSBuild has to be invoked at all.
Now I'm working on a C# project. I wrote a simple MSBuild target which copies a file to the output directory:
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="CopyMyFile" BeforeTargets="AfterBuild" Inputs="$(ProjectDir)File.dat" Outputs="$(TargetDir)FileRenamed.dat">
<Copy SourceFiles="$(ProjectDir)File.dat" DestinationFiles="$(TargetDir)FileRenamed.dat" OverwriteReadOnlyFiles="true">
</Copy>
</Target>
</Project>
The target works as expected if the build is invoked through MSBuild.exe. The file is copied if the target file does not exist or the source file has been modified.
If I invoke the build inside Visual Studio it does not work as expected. Visual Studio does not invoke MSBuild if I delete the file from the output directory. On the other hand MSBuild is invoked every time I build the project after modifiying the source file even if there are no other changes made.
It seems that Visual Studio just compares every file from a project to the output files (.exe, .dll or .pdb). If any file in the project is newer than the output files, MSBuild is invoked. In my case MSBuild does not update the .exe file, so MSBuild is invoked again and again.
In a C++ project this behaviour is controlled by the .tlog files. Is there anything similar in a C# project?
Thanks a lot!
The answer might be no, nothing similar to the tlog mechanism. I am not 100% sure though, also because it's strange you cannot do something quite basic as this as that would mean MS basically ditched the tracker stuff for C# (and similar) projects but didn't replace it with something which can be hooked into by users.
Using procmon you can see VS getting timestamps of output and input files, but nowhere I found a way to interfere with what it treats as input and output files. It looks like VS gets a list of everything included directly in the project file (i.e. Reference/Content/Compile/.. item groups of what is shown in VS), not what is listed in Taget's Inputs/Outputs, and at the start of a build compares timstamps for just those items. If everything (well, everything as far as VS is considered) is up to date no msbuild process is launched for the build.
There is a workaround, though not super nice: if you add a 'dummy' Content item (e.g. Right-click project->Add New Item->Text File) and set it to always be copied (Right-clik text file just added->Properties->Copy to Output Directory->Copy always) then VS will always start a build and hence check your target's Inputs vs the Outputs and run if if you deleted FileRenamed.dat.
It looks like this is just poorly documented. This site shows you can easily hook up a command line tool, while lifting on the incremental features of tlog files.
To make sure the information doesn't get lost, I'll just copy over their use case, but looking at that, I think it's easy to transform into your needs. Every occurrence of dcx can be replaced by e.g. data
1. Create a definition .xml file
Define an ItemType
Link a ContentType to the ItemType
Hook up a FileExtension
<?xml version="1.0" encoding="utf-8"?>
<ProjectSchemaDefinitions xmlns="http://schemas.microsoft.com/build/2009/properties">
<!-- Associate DXCShader item type with .hlsl files -->
<ItemType Name="DXCShader" DisplayName="DXC Shader" />
<ContentType Name="DXCShader" ItemType="DXCShader" DisplayName="DXC Shader" />
<FileExtension Name=".hlsl" ContentType="DXCShader" />
</ProjectSchemaDefinitions>
2. Create a .targets file
Include the .xml definitions file
Create a Target that depends on one of your build hooks (here: ClCompile)
Create an ItemGroup in your Target that will serve as the argument to your CustomBuild. Message, Command, AdditionalInputs and Output are meta-attributes that are relevant.
Invoke CustomBuild with MinimalRebuildFromTracking="true" and a TrackerLogDirectory to contain the tlog files. This part is the magic ingredient that makes MSBuild skip the build if your dependencies are up-to-date.
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<!-- Include definitions from dxc.xml, which defines the DXCShader item. -->
<PropertyPageSchema Include="$(MSBuildThisFileDirectory)dxc.xml" />
<!-- Hook up DXCShader items to be built by the DXC target. -->
<AvailableItemName Include="DXCShader">
<Targets>DXC</Targets>
</AvailableItemName>
</ItemGroup>
<Target
Name="DXC"
Condition="'#(DXCShader)' != ''"
BeforeTargets="ClCompile">
<Message Importance="High" Text="Building shaders!!!" />
<!-- Find all shader headers (.hlsli files) -->
<ItemGroup>
<ShaderHeader Include="*.hlsli" />
</ItemGroup>
<PropertyGroup>
<ShaderHeaders>#(ShaderHeader)</ShaderHeaders>
</PropertyGroup>
<!-- Setup metadata for custom build tool -->
<ItemGroup>
<DXCShader>
<Message>%(Filename)%(Extension)</Message>
<Command>
"$(WDKBinRoot)\x86\dxc.exe" -T vs_6_0 -E vs_main %(Identity) -Fh %(Filename).vs.h -Vn %(Filename)_vs
"$(WDKBinRoot)\x86\dxc.exe" -T ps_6_0 -E ps_main %(Identity) -Fh %(Filename).ps.h -Vn %(Filename)_ps
</Command>
<AdditionalInputs>$(ShaderHeaders)</AdditionalInputs>
<Outputs>%(Filename).vs.h;%(Filename).ps.h</Outputs>
</DXCShader>
</ItemGroup>
<!-- Compile by forwarding to the Custom Build Tool infrastructure,
so it will take care of .tlogs and error/warning parsing -->
<CustomBuild
Sources="#(DXCShader)"
MinimalRebuildFromTracking="true"
TrackerLogDirectory="$(TLogLocation)"
ErrorListRegex="(?'FILENAME'.+):(?'LINE'\d+):(?'COLUMN'\d+): (?'CATEGORY'error|warning): (?'TEXT'.*)" />
</Target>
</Project>

Build configuration for dynamically referenced projects

I have an ASP.NET project that is included in multiple solutions. In each solution I'd like a different unreferenced project to be included in the ASP.NET project's build output. The solutions look like this:
Foo.sln
WebApp.csproj
Foo.csproj
Bar.sln
WebApp.csproj
Bar.csproj
Ideally, this would work even when debugging with F5. I tried doing this with build configurations, but deviating from the typical 'Debug' and 'Release' seems brittle when working within Visual Studio. Is there a typical way of doing this?
Disclaimer: I don't think this is a very good idea to do but it seems like it can be done.
To test this solution I created two projects. ConsoleApplication1 and ClassLibrary1. ConsoleApplication1 does not have a reference (that is visible in Visual Studio) to ClassLibary1 but when building ConsoleApplication1 from Visual Studio it will build then copy the ClassLibary1.dll to the bin folder of ConsoleApplication1.
To import the target file you will go ahead and add this line to the project that you want to build the unreferenced project. This path will be relative to the current project so in my case the target file was at the root of my solution. Make sure you add this after the line <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> because unreferenced.target relies on targets that are setup in Microsoft.CSharp.targets.
<Import Project="..\unreferenced.target" />
Then you will go ahead and create a file name unreferenced.target and add the contents below to the file.
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Start another msbuild task to build your unreferenced project -->
<Target Name="BuildBeforeResolveReferences" BeforeTargets="BeforeResolveReferences">
<MSBuild
BuildInParallel="False"
Projects="$(SolutionDir)ClassLibrary1\ClassLibrary1.csproj"
RunEachTargetSeparately="True"
StopOnFirstFailure="False"
UnloadProjectsOnCompletion="False">
</MSBuild>
</Target>
<Target Name="CopyUnreferencedProjectOutput" AfterTargets="Build">
<!-- This item group is here because we do not want it evaluated by msbuild until the ClassLibrary1.csproj has been compiled and its output is in its output directory -->
<ItemGroup>
<!-- Gets a list of all files at the OutputPath that end in .dll if you need the pdbs remove the .dll -->
<!-- To maintain folder structure in the bin folder use <SourceFiles Include="..\ClassLibary1\#(OutputPath)**\*.dll" /> the double ** is a recursive wild card and will look through all directorys -->
<SourceFiles Include="$(MSBuildProjectDirectory)\..\ClassLibrary1\$(OutputPath)*.dll" />
</ItemGroup>
<!-- To make sure the copy maintains folder structure switch it to this copy -->
<!-- <Copy SourceFiles="#(SourceFiles)" DestinationFiles="#(SourceFiles -> '$(MSBuildProjectDirectory)$(OutputPath)%(RecursiveDir)%(Filename)%(Extension)')" /> -->
<Copy SourceFiles="#(SourceFiles)" DestinationFolder="$(MSBuildProjectDirectory)\$(OutputPath)" />
</Target>
<!-- Cleans up all the files when clean is called -->
<Target Name="CleanUnreferenceProjectOutput" BeforeTargets="Clean">
<ItemGroup>
<!-- Removed the .dll from the end of this to clean up the pdbs as well -->
<SourceFiles Include="$(SolutionDir)\ClassLibrary1\$(OutputPath)*" />
<SourceFiles Include="$(SolutionDir)\ConsoleApplication1\$(OutputPath)*.dll" />
</ItemGroup>
<Delete Files="#(SourceFiles)" />
</Target>
</Project>
I think this is the best that can be done. You could extend this to have a list of projects that are not referenced but you want to build but for this example I just left it at one.
EDIT 2: Before getting to the current solution I did extensive research into injecting the reference into the ProjectReference itemgroup before assemblies were resolved. It can be done but you have to set the property BuildInVisualStudio to false because otherwise when the msbuild conditions are evaluted in the ResolveProjectReferences target in Microsoft.Common.Current.targets you will select a MSBuild task that only runs the GetManifest target. I was able to get the solution to build but given my lack of knowledge on what setting BuildInVisualStudio to false entails I opted for the solution above. Also I added a task for cleaning up the files that were moved to the bin folders because clean will only cleanup what {ProjectName}{ProjectExtension}FileListAbsoluteText.txt in the obj folder of your project.
EDIT: After doing some more research into the solution below it will only work from the command line. I am currently looking into why this is occuring.
I don't know if there is a typical way of doing what you are asking for (from IDE), but you have an options to accomplish this manually by editing the *.*proj files.
Each project will emit output (*.dll, *.exe, app.config, etc), and it will be copied to the folder specified in the $(OutputPath) property (internally it will use OutDir property). If you will build a solution, you will have the $(SolutionDir) property, as well as $(SolutionName). So, you can define new msbuild project, which will be referenced by the other ones, and you can set the property $(OutputPath) so that every output will go into one folder (let call it Common.props):
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<SolutionDir Condition=" '$(SolutionDir)' == '' ">$(MSBuildThisFileDirectory)<SolutionDir>
<SolutionName Condition=" '$(SolutionName)' == '' " >DefaultSlnName</SolutionName>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<OutputPath>$(SolutionDir)$(SolutionName)\bin\$(Configuration)</OutputPath>
</PropertyGroup>
</Project>
After that, you should import that project by your other projects - *.*proj (you should specify correct path to the project):
<Import Project="..\Common.props" Condition="Exists('..\Common.props')" />
Using common $(OutputPath) property will place all of your binaries to the one folder - this should help to resolve your task.

Can you use msbuild batching in AdditionalProperties or Properties Item Metadata - specifically the Configurations property.?

I'm trying to use batching in the AdditionalProperties metadata to build a csproj twice one for Debug and one for Build using the Configuration property.
Traditionally you see this:
<ItemGroup>
<MyConfigurations Include="Release"/>
<MyConfigurations Include="Debug"/>
</ItemGroup>
<Task>
<MSBuild Projects="#(ProjectsToBuild)"
BuildInParallel="true"
Configurations="%(MyConfigurations.Identity)"
/>
</Task>
But what I want is something like this:
<Project ToolsVersion="12.0" DefaultTargets="CoreBuild" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
...
<ItemGroup>
<MyConfigurations Include="Release"/>
<MyConfigurations Include="Debug"/>
</ItemGroup>
<ItemGroup>
<ProjectsToBuild Include="$(SourcePath)\somedir\project1.csproj">
<AdditionalProperties>Configuration=%(MyConfigurations.Identity);PlatformTarget=x86</AdditionalProperties>
</ProjectsToBuild>
<ProjectsToBuild Include="$(RootPath)\someOtherMSbuildProject.proj">
<AdditionalProperties>
PackageRoot=$(PackageRoot);
PackagePassword=$(PackagePassword);
</AdditionalProperties>
</ProjectsToBuild>
</ItemGroup>
<Target Name="CoreBuild">
<MSBuild
Projects="#(ProjectsToBuild)"
BuildInParallel="true"
/>
</Target>
That way I can do a parallel build from what I understand. I'm just a beginner in MSBUILD. From what I understand the first way of batching will prevent a parallel build. But when I try to do this I get an error: "The output path is not specified for project Project1.csproj..." and then further on in the error message it tells me "Configuration=' ' Platform='x86'". The error message then continues on saying I may not have set Configuration and Platform correctly. Looks like that's right since it's saying nothing is set for Configuration.
Is what I'm trying to do even possible? I thought it was. I searched quite a bit and I found this solution here: MSDN: MSBuild task: Batching and BuildInParallel=true. This link is similar to what I tried to do but instead of using Configuration in the AdditionalProperties metadata like so:
Configuration="%(MyConfigurations.Identity)"
they use Config like
Config="%(MyConfigurations.Identity)".
I don't know where Config comes from or why they use it. When I try to use it only the Debug will build.
If I create a separate Item for the csproj - one for Configuration=Debug and one for Configuration=Release then it works as expected. But I'm looking for a way to prevent having to create two separate items for the .csproj just to get the two builds.
I tried so much stuff now that I'm a bit confused. I couldn't find any mention of a "Config" parameter other than the above MSDN link. I also have Sayad's Inside the Microsoft Build Engine book and I can't find anything there.
I'm using visual studio 13 with 12.0 toolsversion.
OK I feel dumb. The Items have to be located in a Target element in order for batching to work. So in other words batching only applies to dynamic items.
So like this:
<Target>
<ItemGroup>
<ProjectsToBuild2 Include="$(SourcePath)\RapidCoreConfigurator\RapidCoreConfigurator.csproj">
<AdditionalProperties>Configuration=%(MyConfigurations.Identity);PlatformTarget=x86</AdditionalProperties>
</ProjectsToBuild2>
</ItemGroup>
<Message Text="MyConfigurations: %(MyConfigurations.Identity)"/>
<MSBuild
Projects="#(ProjectsToBuild2)"
BuildInParallel="true"
/>
</Target>
I did try that but I must had made some mistake; I was trying so much stuff.
I'm not sure why fellow in the linked article used "Config" instead of "Configuration" but he might have made a mistake or have been going from memory.

Determine if MSBuild CoreCompile will run and call custom target

This seems like an obvious thing to want to do but I have pulled most of my hair out trying to find any examples on the web or do it myself.
I have a c# solution with 19 projects and a Jenkins build server running a build script to drive MSBuild. MSBuild will of course determine what does and does not need to be compiled based on inputs versus outputs.
I am trying to create a custom target to conditionally update the AssemblyInfo.cs of those projects MSBuild is going to compile to increment the file versions. Of course I want to leave the projects not being compiled alone.
I know how to inject a target prior to the CoreBuild that runs every time so if there is some variable I can test to see if a compile will occur that can work. I also know how to determine if a compile ran and therefore conditionally do some post processing which is possible but not ideal.
How can I tweak my build process to achieve this?
Since it seems there's no straight answer to the question, does anyone know how to perform the same logic as MSBuild to determine what projects require a rebuild?
In the end the solution was a combination of Sayed Ibrahim Hashimi's blog entry and information from the MSDN Forum entry 'Execute target when (core)compile will execute'.
I basically took Sayed's injection method to get my target to run 'extend-corecompile.proj' on all projects without having to edit each proj file but replaced it's contents with an override for 'CoreCompileDependsOn' that points to a custom target that adopts the same inputs and outputs as the 'CoreCompile' target. The end result is a target that only runs when 'CoreCompile' will run while being centrally managed in the build script.
Thanks to all for their input and here is the skeleton code I used in 'extend-corecompile.proj':
<!--The following property group adds our custom post-target to the post compile call list -->
<PropertyGroup>
<TargetsTriggeredByCompilation>
$(TargetsTriggeredByCompilation);
CustomPostTarget
</TargetsTriggeredByCompilation>
</PropertyGroup>
<!--The following property group adds our custom pre-target to CoreCompileDependsOn to ensure it is called before CoreCompile -->
<PropertyGroup>
<CoreCompileDependsOn>
$(CoreCompileDependsOn);
CustomPreTarget
</CoreCompileDependsOn>
</PropertyGroup>
<!-- The following custom pre-target has the same inputs and outputs as CoreCompile so that it will only run when CoreCompile runs.
Because we have injected this file and Targets are resolved in sequence we know this Target will fire before CoreCompile.-->
<Target Name="CustomPreTarget"
Inputs="$(MSBuildAllProjects);
#(Compile);
#(_CoreCompileResourceInputs);
$(ApplicationIcon);
$(AssemblyOriginatorKeyFile);
#(ReferencePath);
#(CompiledLicenseFile);
#(EmbeddedDocumentation);
$(Win32Resource);
$(Win32Manifest);
#(CustomAdditionalCompileInputs)"
Outputs="#(DocFileItem);
#(IntermediateAssembly);
#(_DebugSymbolsIntermediatePath);
$(NonExistentFile);
#(CustomAdditionalCompileOutputs)">
<!--Do pre-compilation processing here-->
</Target>
<!--This target will be called by CoreCompile-->
<Target Name="CustomPostTarget" >
<!--Do post-compilation processing here-->
</Target>
Not sure what will happen if CoreCompile fails, does it still call our target? I guess in time we'll find out :)
I just blogged the answer to this at http://sedodream.com/2012/07/28/MSBuildHowToExecuteATargetAfterCoreCompilePart2.aspx but I've pasted the solution for you below.
A couple of months ago I wrote a blog post MSBuild how to execute a target after CoreCompile in which I describe how you can execute a target if the CoreCompile target is executed, if CoreCompile is skipped then so will your other target. The draw back of the approach that I outlined in my previous post was that it required you to edit your .csproj/.vbproj/etc file itself. So if you had a scenario where you were building multiple projects then you would have to edit all of the project files. In this post I’ll describe how you can perform the same customization without having to edit the project file itself.
Before we get to the solution for this particular case let me describe an extensibility hook that the C# and VB projects have. Most of the logic for building C# and VB projects is captured in the MSBuild targets file at C:\Windows\Microsoft.NET\Framework\v4.0.30319\Microsoft.Common.targets. If you take a look in that file you will notice at the top an import looking like the one below.
<Import Project="$(CustomBeforeMicrosoftCommonTargets)" Condition="'$(CustomBeforeMicrosoftCommonTargets)' != '' and Exists('$(CustomBeforeMicrosoftCommonTargets)')"/>
This statement will import a file (located at the value for CustomBeforeMicrosoftCommonTargets) if the property is not empty and the file exists. The default value for CustomBeforeMicrosoftCommonTargets is C:\Program Files (x86)\MSBuild\v4.0\Custom.Before.Microsoft.Common.targets. So if you drop an MSBuild file at that location it will modify the build process for every C#/VB project built on that machine. Alternatively if you do not want (or cannot due to ACLs) then you can drop the file somewhere else and then specify its location by overriding the CustomBeforeMicrosoftCommonTargets property. This is the approach that I will take here. I have created a sample solution which consists of two projects ProjA and ProjB. I also have a build script, build.proj, to automate the build for this. Below is the entire contents of build.proj.
build.proj
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<FileToInject Condition=" '$(FileToInject)'=='' ">$(MSBuildThisFileDirectory)extend-corecompile.proj</FileToInject>
</PropertyGroup>
<ItemGroup>
<ProjectsToBuild Include="ProjA\ProjA.csproj"/>
<ProjectsToBuild Include="ProjB\ProjB.csproj"/>
</ItemGroup>
<Target Name="Build">
<MSBuild Projects="#(ProjectsToBuild)"
Properties="CustomBeforeMicrosoftCommonTargets=$(FileToInject)" />
</Target>
<Target Name="Clean">
<MSBuild Projects="#(ProjectsToBuild)" Targets="Clean"/>
</Target>
<Target Name="Rebuild" DependsOnTargets="Clean;Build"/>
</Project>
In the Build target above I use the MSBuild task to build both ProjA and ProjB. As you can see I am passing the property CustomBeforeMicrosoftCommonTargets=$(FileToInject) which points to extend-corecompile.proj. By passing this property when ProjA, and ProjB, is built it will automatically import the extend-corecompile.proj file for the build process. You can see the contents of extend-corecompile.proj below.
extend-corecompile.proj
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<TargetsTriggeredByCompilation>
$(TargetsTriggeredByCompilation);
MyCustomTarget
</TargetsTriggeredByCompilation>
</PropertyGroup>
<Target Name="MyCustomTarget">
<Message Text="MyCustomTarget called" Importance ="high"/>
</Target>
</Project>
This project file uses the technique outlined in my previous blog post to execute the MyCustomTarget only if CoreCompile is executed.
Note: You can get the latest version of this sample at https://github.com/sayedihashimi/sayed-samples/tree/master/ExtBuildMultiple.
Alternately, you can use a single auto-generated VersionInfo.cs file that is referenced by all of the projects. To use this technique, strip out the version, company info, etc. attributes from your projects' AssemblyInfo.cs file (yes, this is a pain, but you only have to do this once), and have a batch command spit out a VersionInfo.cs file based on a template. To reference the common file in Visual Studio, you choose Add Existing Item from the project context menu, and after you've navigated to the VersionInfo.cs file in the file browser, click the drop-down arrow next to Add and select Add as Link.
Below is an example of one I use. This script is checked into our SCC system and is executed at the beginning of the build, supplying %BUILD_NUMBER% to the script.
SET BUILD=%1
#echo using System.Reflection; > "%~p0Version.cs"
#echo [assembly: AssemblyCompany("MyCompany, Inc.")] >> "%~p0Version.cs"
#echo [assembly: AssemblyProduct("MyProduct")] >> "%~p0Version.cs"
#echo [assembly: AssemblyCopyright("Copyright © 2012 MyCompany, Inc.")] >> "%~p0Version.cs"
#echo [assembly: AssemblyTrademark("")]#echo [assembly: AssemblyVersion("1.0.%BUILD%.0")] >> "%~p0Version.cs"
#echo [assembly: AssemblyFileVersion("1.0.%BUILD%.0")] >> "%~p0Version.cs"
#echo ^<Include xmlns="http://schemas.microsoft.com/wix/2006/wi"^> > "%~p0Version.wxi"
#echo ^<?define VersionBuild="%BUILD%"?^> >> "%~p0Version.wxi"
#echo ^</Include^> >> "%~p0\Version.wxi"
Even if you got the list of projects needing compilation, if you update the assemblyinfo.cs of one of them, it may induce a change that triggers a compilation of another project.
So, simpliest way is to generate all AssemblyInfo.cs files according to source control revision number. You could even get latest revision number for each project directory, effectively knowing when was the "last" modification on this project.
See this question : How can I change AssemblyProduct, AssemblyTitle using MSBuild?
According to your comment, have you looked into the BeforeBuild and AfterBuild targets (at the end of your csproj file) :
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>

external file for build tasks

I am creating a .net web application and have some build tasks eg msbuild copy task. I add this by editing the project file for the application and adding the task.
While this works ok, is there any way I can use an external xml file to the project file and have my build tasks in this with the main project file referencing it? I would much prefer this as I wouldn't then have to edit the main project file and there is separation between the project file and the build tasks.
You can simply reference any external project or target file by adding an import to your main project file:
...
<!-- this is the default import for (c#) web project files -->
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v9.0\WebApplications\Microsoft.WebApplication.targets" />
<!-- import your custom project/target file here -->
<Import Project="MyCustom.targets" Condition="Exists('MyCustom.targets')" />
...
Adding the Condition will allow to build your main project even if your custom project/target file is missing because the build is run in a different environment.
I'm using this approach to run FxCop and StyleCop targets on my local machine but the same main project file can be built without any changes in my staging environment.
Update
Your comment suggests that you actually are looking rather for a solution that should work the other way round: You want to execute some steps before and after building your project without modifying the project configuration itself.
In that case the best way is to create your custom project to call the build of your web project. This could look something like this:
<Project DefaultTargets="MyTargetAfterBuild">
<!-- some Project attributes omitted for readability -->
<Target Name="MyTargetBeforeBuild" ContinueOnError="false">
<Exec Command="svn export" />
</Target>
<Target Name="Build" DependsOnTargets="MyTargetBeforeBuild">
<MSBuild Projects="MyWebProject.csproj" Targets="Build" Properties="Configuration=Release" >
</MSBuild>
</Target>
<Target Name="MyTargetAfterBuild" DependsOnTargets="Build">
<Exec Command="powershell.exe .\MyCustomScript.ps1" />
</Target>
</Project>
You might be interested in this answer on a similar scenario with a more detailed example.
You can call msbuild.exe, targetting your separate build file, in a pre-build or post-build event.

Categories

Resources