Custom common target to build a solution

Benjamin Baumann picture Benjamin Baumann · Aug 3, 2010 · Viewed 11.1k times · Source

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?

Answer

Ade Miller picture Ade Miller · Aug 7, 2010

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.