Search code examples
buildmsbuildsolution

Use ProjectName when building an entire solution with MSBuild


Rather than building to the default folder structure, which is like

Solution.sln
Project1
    bin    <- project 1 output
    obj    <- project 1 intermediate output
Project2
    bin    <- project 2 output
    obj    <- project 2 intermediate output

I instead want to build it like

Solution.sln
bin    <- project 1 AND 2 output
obj
    Project1   <- project 1 intermediate output
    Project2   <- project 2 intermediate output

I can do

msbuild "/p:OutputPath=../bin" "/p:IntermediateOutputPath=../obj/" Test123.sln

However, using "/p:IntermediateOutputPath=../obj/$(ProjectName)/" does not work. Instead of creating a folder for each project, it creates one folder literally called $(ProjectName) (I've read that most, but not all of these macros are actually a Visual Studio thing, rather than MSBuild magic).

How can I use a project-specific value (such as ProjectName) in a property value (such as IntermediateOutputPath) when building?

(Some background information:

Having one bin folder on the solution level saves unnecessary copying of output files, which quickly amass over 100 MB in large solutions. Furthermore, it keeps the source folders clean so they can be read-only.

I still want separate obj folders though, because who knows what goes in there - might be the same file names for different projects.)


Solution

  • You can override the CustomAfterMicrosoftCommonTargets property of the Microsoft.Common.targets file. It's allowing injecting custom targets into projects and performs some actions.


    The entry point of the build process is Make.targets:

    <Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"; DefaultTargets="EntryMSBuild">
    
        <ItemGroup>
            <Project Include="**\*.csproj"/>
        </ItemGroup>
    
        <Target Name="EntryMSBuild">
    
            <Message Text="-----Entry-----" Importance="high"/>
            <Message Text="    Rebuild    " Importance="high"/>
            <Message Text="-----Entry-----" Importance="high"/>
    
            <MSBuild Projects="@(Project)" Targets="rebuild" Properties="CustomBeforeMicrosoftCommonTargets=$(MSBuildThisFileDirectory)Setting.targets"/>
        </Target>
    
    </Project>
    

    In Setting.targets defined IntermediateOutputPath and OutputPath for each projects:

    <Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    
        <PropertyGroup>
            <IntermediateOutputPath>..\Global\obj\$(MSBuildProjectName)</IntermediateOutputPath>
        </PropertyGroup>
    
        <PropertyGroup>
            <OutputPath>..\Global\bin\</OutputPath>
        </PropertyGroup>
    
    </Project>
    

    As a result, you get the desired structure:

    Solution.sln
    bin    <- project 1 AND 2 output
    obj
        Project1   <- project 1 intermediate output
        Project2   <- project 2 intermediate output
    

    Tested on solution with two projects under .net46 and .netstandard2.0. MSBuild 15.6.82.30579


    You don’t need store custom targets under C:\Program Files[(x86)]\microsoft visual studio\2017\xxx\msbuild\15.0 or any predefined path. You override CustomBeforeMicrosoftCommonTargets property that already defined in Microsoft.Common.targets and injected in the project by default.

    From Microsoft.Common.targets comment:

    This file defines the steps in the standard build process for .NET projects. It contains all the steps that are common among the different .NET languages, such as Visual Basic, and Visual C#.