MzunguMark
MzunguMark

Reputation: 175

Why is MSBuild rebuilding shared projects?

I am trying to speed up the compilation of a set of solutions by only compiling shared projects once. This is a simplified description of my problem.

I have two solutions -- one.sln and two.sln. They both include shared project shared.csproj. I open the two solutions in Visual Studio, clean both, then build one.sln, then build two.sln. When two.sln is built, shared.dll is NOT regenerated, which is the behavior I expect.

To automate this process, I have created an msbuild .proj file (below). When I call msbuild on the .proj file, shared.dll is recompiled for BOTH solutions. What am I doing wrong?

<Project ToolsVersion="4.0" 
         xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
         DefaultTargets="Build">
    <ItemGroup>
        <Solution Include="C:\one.sln"/>
        <Solution Include="C:\two.sln"/>
    </ItemGroup>
    <Target Name="Build">
      <MSBuild Projects="@(Solution)" Targets="Build" RunEachTargetSeparately="false" StopOnFirstFailure="true" />
    </Target>
 </Project>

Upvotes: 5

Views: 1199

Answers (3)

Chui Tey
Chui Tey

Reputation: 5554

Visual Studio may make rebuilds a little harder to reason because it has a "Fast Up-To-Date checker" that has custom heuristics. You can find out why a project is being rebuilt by enabling the verbosity of the fast up to date checker in the registry key:

Visual Studio 2017 (must be running) [2]

New-ItemProperty              `
 -Name U2DCheckVerbosity      `
 -PropertyType DWORD -Value 1 `
 -Path HKCU:\Software\Microsoft\VisualStudio\15.0\General -Force

Visual Studio 2015

New-ItemProperty              `
 -Name U2DCheckVerbosity      `
 -PropertyType DWORD -Value 1 `
 -Path HKCU:\Software\Microsoft\VisualStudio\14.0\General -Force

You should be able to see in the build log messages like

Project 'Caliburn.Micro.Silverlight.Extensions' is not up to date. Project item 'C:\dev\projects\Caliburn.Micro.Silverlight.Extensions\NavigationBootstrapperSample.cs.pp' has 'Copy to Output Directory' attribute set to 'Copy always'.

[1] https://blogs.msdn.microsoft.com/kirillosenkov/2014/08/04/how-to-investigate-rebuilding-in-visual-studio-when-nothing-has-changed/

[2] https://visualstudioextensions.vlasovstudio.com/2017/06/29/changing-visual-studio-2017-private-registry-settings/ Visual Studio 2017 uses private registry hive

Upvotes: 0

MzunguMark
MzunguMark

Reputation: 175

I figured out why the shared projects were being recompiled for every solution. We have a post build step in each project that code signs the target file. After each previous compilation of a project, the .dll file is newer than the .pdb file, so it recompiles the project. I found this out by using the /v:d switch for verbose logging, then searching for "completely" in the output to find out why projects were being built completely each time.

Upvotes: 1

CheGueVerra
CheGueVerra

Reputation: 7963

IIRC, the reason why the reference is compiled twice, is that it is declared as a reference project in both solutions. So both will call the Build Target of their reference. That's how I understand it.

Create an Itemgroup for the reference project

  <!--Project reference-->
  <ItemGroup>
    <ProjectReference Include="refProject\refProject.csproj">
      <Targets>Build</Targets>
    </ProjectReference>
  </ItemGroup>

Add a target to build Reference

  <Target Name="ComputeProjectReference" Inputs="@(ProjectReference)" Outputs="%(ProjectReference.Identity)__Forced">
    <MSBuild Projects="@(ProjectReference)" Targets="%(ProjectReference.Targets)">
      <Output TaskParameter="TargetOutputs" ItemName="ResolvedProjectReferences"/>
    </MSBuild>
  </Target>

  <Target Name="AfterProjectReference"  AfterTargets="ComputeProjectReference">
    <CreateItem Include="@(ResolvedProjectReferences)">
      <Output TaskParameter="Include" ItemName="CopyFiles" />
    </CreateItem>

    <Copy SourceFiles="@(CopyFiles)" DestinationFolder="$(AssemblyName)\$(OutputPath)" SkipUnchangedFiles="false"  />

    <ItemGroup>
      <NewAssemblies Include="$(AssemblyName)\$(OutputPath)%(CopyFiles.FileName)%(CopyFiles.Extension)" />
    </ItemGroup>

  </Target>

Unfortunately, MSBuild task doesn't take references as a parameter. I suggest creating itemGroups that represent each project required. something like

  <ItemGroup>
    <CompileA Include="ConsProject\Program.cs" />
    <CompileA Include="ConsProject\Properties\AssemblyInfo.cs" />
    <CompileA Include="ConsProject\Properties\Settings.Designer.cs">
      <AutoGen>True</AutoGen>
      <DesignTimeSharedInput>True</DesignTimeSharedInput>
      <DependentUpon>Settings.settings</DependentUpon>
    </CompileA>
  </ItemGroup>

  <ItemGroup>
    <CompileB Include="OtherProject\Program.cs" />
    <CompileB Include="OtherProject\Properties\AssemblyInfo.cs" />
    <CompileB Include="OtherProject\Properties\Settings.Designer.cs">
      <AutoGen>True</AutoGen>
      <DesignTimeSharedInput>True</DesignTimeSharedInput>
      <DependentUpon>Settings.settings</DependentUpon>
    </CompileB>
  </ItemGroup>

And create a target to build the projects with the references compiled once

  <!--Build Process-->
  <Target Name="Build" DependsOnTargets="ComputeProjectReference" >
    <Csc Sources="@(CompileA)" References="@(NewAssemblies)" TargetType="exe" OutputAssembly="$(AssemblyName)\$(OutputPath)\$(AssemblyName).exe" />

    <Csc Sources="@(CompileB)" References="@(NewAssemblies)" TargetType="exe" OutputAssembly="$(AssemblyName)\$(OutputPath)\$(AssemblyName).exe" />

  </Target>

Upvotes: 3

Related Questions