dotnet / msbuild

The Microsoft Build Engine (MSBuild) is the build platform for .NET and Visual Studio.
https://docs.microsoft.com/visualstudio/msbuild/msbuild
MIT License
5.23k stars 1.35k forks source link

Difference when building VS solution in VS and msbuild #417

Open fedeazzato opened 8 years ago

fedeazzato commented 8 years ago

I'm having an issue building some projects. I´ve found differences between building my solutions in VS (it work fine), and building the solutions with msbuild through command line (it fails).

I've managed to make a small sample that illustrates the problem. BuildError.zip

Basically I've got 2 solutions SolA.sln and SolAandB.sln. SolA.sln only contains project A.csproj while SolAandB.sln contains both A.csproj and B.csproj. The project A.csproj is just a class library, while B.csproj is a command line project which references project A.csproj. The subtle peculiarity about this projects, is that A.csproj is configured to not be built in SolAandB.sln. The reason of this, is that I've got a lot of projects (more than 300, some of which are C# and others are C++ managed and unmanaged), and some projects are included in more than one solution (mainly to allow adding references to the project), but I only want to build the project once.

When I build SolAandB.sln in VS (I'm using VS2015 with update 1, but don't believe that changes anything), I can see that the invocation to csc.exe for B.csproj is as follows (I've added line breaks between arguments for clarity):

C:\Program Files (x86)\MSBuild\14.0\bin\csc.exe
 /noconfig
 /nowarn:1701,1702,2008
 /nostdlib+
 /platform:anycpu32bitpreferred
 /errorreport:prompt
 /warn:4
 /define:DEBUG;TRACE
 /errorendlocation
 /preferreduilang:en-US
 /highentropyva+
 /reference:"F:\Visual Studio 2015\Projects\BuildError\A\bin\Debug\A.dll"
 /reference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5.2\mscorlib.dll"
 /reference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5.2\System.Core.dll"
 /debug+
 /debug:full
 /filealign:512
 /optimize-
 /out:obj\Debug\B.exe
 /ruleset:"C:\Program Files (x86)\Microsoft Visual Studio 14.0\Team Tools\Static Analysis Tools\\Rule Sets\MinimumRecommendedRules.ruleset"
 /subsystemversion:6.00
 /target:exe
 /utf8output
 Program.cs
 Properties\AssemblyInfo.cs
 "C:\Users\Fede\AppData\Local\Temp\.NETFramework,Version=v4.5.2.AssemblyAttributes.cs"
 obj\Debug\\TemporaryGeneratedFile_E7A71F73-0F8D-4B9B-B56E-8E70B10BC5D3.cs
 obj\Debug\\TemporaryGeneratedFile_036C0B5B-1481-4323-8D20-8F5ADCB23D92.cs
 obj\Debug\\TemporaryGeneratedFile_5937a670-0e60-4077-877b-f7221da3dda1.cs

Note the /reference:"F:\Visual Studio 2015\Projects\BuildError\A\bin\Debug\A.dll" which points correctly to the output of A.csproj for the selected build configuration. If I had built in Release, the reference would have correctly pointed to the release build output of A.csproj.

The result of that build depends on having A.csproj already built, which I have. You can see a sample build script in BuildSolutions.proj which first builds SolA.sln and then SolAandB.sln.

Now if I build SolAandB.sln through the command line, or as a result of building BuildSolutions.proj, I can see that the invocation to csc.exe for B.csproj is as follows:

C:\Program Files (x86)\MSBuild\14.0\bin\csc.exe
 /noconfig
 /nowarn:1701,1702
 /nostdlib+
 /platform:anycpu32bitpreferred
 /errorreport:prompt
 /warn:4
 /define:DEBUG;TRACE
 /highentropyva+
 /reference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5.2\mscorlib.dll"
 /reference:"C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5.2\System.Core.dll"
 /debug+
 /debug:full
 /filealign:512
 /optimize-
 /out:obj\Debug\B.exe
 /ruleset:"C:\Program Files (x86)\Microsoft Visual Studio 14.0\Team Tools\Static Analysis Tools\\Rule Sets\MinimumRecommendedRules.ruleset"
 /subsystemversion:6.00
 /target:exe
 /utf8output
 Program.cs
 Properties\AssemblyInfo.cs
 "C:\Users\Fede\AppData\Local\Temp\.NETFramework,Version=v4.5.2.AssemblyAttributes.cs"

As you can see, beside several apparently subtle differences, the biggest difference is in that the argument /reference:"F:\Visual Studio 2015\Projects\BuildError\A\bin\Debug\A.dll" is missing, and thus the project fails to build with the error error CS0246: The type or namespace name 'A' could not be found (are you missing a using directive or an assembly reference?).

How can I build the solution, in a way that when csc.exe is invoked, it's invoked the same way as when I'm building the solution inside visual studio?

rainersigwald commented 8 years ago

This does look like a bug, thanks for pointing it out.

Some further information from a /v:diag build log. The reference isn't added because it doesn't get computed in ResolveProjectReferences

Target "ResolveProjectReferences: (TargetId:83)" in file "C:\Program Files (x86)\MSBuild\14.0\bin\Microsoft.Common.CurrentVersion.targets" from project "C:\BuildError\B\B.csproj" (target "ResolveReferences" depends on it):
Task "MSBuild" skipped, due to false condition; ('%(_MSBuildProjectReferenceExistent.BuildReference)' == 'true' and '@(ProjectReferenceWithConfiguration)' != '' and ('$(BuildingInsideVisualStudio)' == 'true' or '$(BuildProjectReferences)' != 'true') and '$(VisualStudioVersion)' != '10.0' and '@(_MSBuildProjectReferenceExistent)' != '') was evaluated as ('false' == 'true' and '..\A\A.csproj' != '' and ('' == 'true' or 'true' != 'true') and '14.0' != '10.0' and '..\A\A.csproj' != '').
Task "MSBuild" skipped, due to false condition; ('%(_MSBuildProjectReferenceExistent.BuildReference)' == 'true' and '@(ProjectReferenceWithConfiguration)' != '' and ('$(BuildingInsideVisualStudio)' == 'true' or '$(BuildProjectReferences)' != 'true') and '$(VisualStudioVersion)' == '10.0' and '@(_MSBuildProjectReferenceExistent)' != '') was evaluated as ('false' == 'true' and '..\A\A.csproj' != '' and ('' == 'true' or 'true' != 'true') and '14.0' == '10.0' and '..\A\A.csproj' != '').
Task "MSBuild" skipped, due to false condition; ('%(_MSBuildProjectReferenceExistent.BuildReference)' == 'true' and '@(ProjectReferenceWithConfiguration)' != '' and '$(BuildingInsideVisualStudio)' != 'true' and '$(BuildProjectReferences)' == 'true' and '@(_MSBuildProjectReferenceExistent)' != '') was evaluated as ('false' == 'true' and '..\A\A.csproj' != '' and '' != 'true' and 'true' == 'true' and '..\A\A.csproj' != '').
Task "MSBuild" skipped, due to false condition; ('%(_MSBuildProjectReferenceExistent.BuildReference)' == 'true' and '@(ProjectReferenceWithConfiguration)' != '' and '$(BuildingProject)' == 'true' and '@(_MSBuildProjectReferenceExistent)' != '') was evaluated as ('false' == 'true' and '..\A\A.csproj' != '' and 'true' == 'true' and '..\A\A.csproj' != '').
Task "Warning" skipped, due to false condition; ('@(ProjectReferenceWithConfiguration)' != '' and '@(_MSBuildProjectReferenceNonexistent)' != '') was evaluated as ('..\A\A.csproj' != '' and '' != '').
Done building target "ResolveProjectReferences" in project "B.csproj".: (TargetId:83)

Because the project has metadata BuildReference=false from:

Target "AssignProjectConfiguration: (TargetId:81)" in file "C:\Program Files (x86)\MSBuild\14.0\bin\Microsoft.Common.CurrentVersion.targets" from project "C:\BuildError\B\B.csproj" (target "ResolveReferences" depends on it):
Set Property: OnlyReferenceAndBuildProjectsEnabledInSolutionConfiguration=true
Set Property: ShouldUnsetParentConfigurationAndPlatform=true
Set Property: AddSyntheticProjectReferencesForSolutionDependencies=true
Task "AssignProjectConfiguration" (TaskId:47)
  Task Parameter:
      ProjectReferences=
          ..\A\A.csproj
                  Name=A
                  OutputItemType=
                  Project={df198bff-9ad4-45bd-8152-d92259567466}
                  ReferenceSourceTarget=ProjectReference
                  Targets= (TaskId:47)
  Task Parameter:CurrentProject=C:\BuildError\B\B.csproj (TaskId:47)
  Task Parameter:CurrentProjectConfiguration=Debug (TaskId:47)
  Task Parameter:CurrentProjectPlatform=AnyCPU (TaskId:47)
  Task Parameter:OutputType=Exe (TaskId:47)
  Task Parameter:ResolveConfigurationPlatformUsingMappings=False (TaskId:47)
  Task Parameter:SolutionConfigurationContents=<SolutionConfiguration>
    <ProjectConfiguration Project="{DF198BFF-9AD4-45BD-8152-D92259567466}" AbsolutePath="C:\BuildError\A\A.csproj" BuildProjectInSolution="False">Debug|AnyCPU</ProjectConfiguration>
    <ProjectConfiguration Project="{0D8A39A6-7DEF-45D4-86E6-41E1FA9BCC8C}" AbsolutePath="C:\BuildError\B\B.csproj" BuildProjectInSolution="True">Debug|AnyCPU</ProjectConfiguration>
  </SolutionConfiguration> (TaskId:47)
  Task Parameter:AddSyntheticProjectReferencesForSolutionDependencies=True (TaskId:47)
  Task Parameter:OnlyReferenceAndBuildProjectsEnabledInSolutionConfiguration=True (TaskId:47)
  Task Parameter:ShouldUnsetParentConfigurationAndPlatform=True (TaskId:47)
  Project reference "..\A\A.csproj" has been assigned the "Debug|AnyCPU" configuration. (TaskId:47)
  Output Item(s): 
      _ProjectReferenceWithConfiguration=
          ..\A\A.csproj
                  BuildReference=false
                  Configuration=Debug
                  FullConfiguration=Debug|AnyCPU
                  Name=A
                  OutputItemType=
                  Platform=AnyCPU
                  Project={df198bff-9ad4-45bd-8152-d92259567466}
                  ReferenceOutputAssembly=false
                  ReferenceSourceTarget=ProjectReference
                  SetConfiguration=Configuration=Debug
                  SetPlatform=Platform=AnyCPU
                  Targets= (TaskId:47)
  Output Item(s): 
      ProjectReferenceWithConfiguration=
          ..\A\A.csproj
                  BuildReference=false
                  Configuration=Debug
                  FullConfiguration=Debug|AnyCPU
                  Name=A
                  OutputItemType=
                  Platform=AnyCPU
                  Project={df198bff-9ad4-45bd-8152-d92259567466}
                  ReferenceOutputAssembly=false
                  ReferenceSourceTarget=ProjectReference
                  SetConfiguration=Configuration=Debug
                  SetPlatform=Platform=AnyCPU
                  Targets= (TaskId:47)
Done executing task "AssignProjectConfiguration". (TaskId:47)
Done building target "AssignProjectConfiguration" in project "B.csproj".: (TargetId:81)

That's set here.

Setting MSBUILDEMITSOLUTION=1, gives this in the .sln.metaproj:

    <CurrentSolutionConfigurationContents>
      <SolutionConfiguration xmlns="">
  <ProjectConfiguration Project="{DF198BFF-9AD4-45BD-8152-D92259567466}" AbsolutePath="C:\BuildError\A\A.csproj" BuildProjectInSolution="False">Debug|AnyCPU</ProjectConfiguration>
  <ProjectConfiguration Project="{0D8A39A6-7DEF-45D4-86E6-41E1FA9BCC8C}" AbsolutePath="C:\BuildError\B\B.csproj" BuildProjectInSolution="True">Debug|AnyCPU</ProjectConfiguration>
</SolutionConfiguration>
    </CurrentSolutionConfigurationContents>

So I think the culprit is that BuildProjectInSolution="False" is interpreted a bit too strictly.

I don't know yet what the best fix would be. Maybe stop checking that condition for the GetTargetPath invocation? But we'd need to think through the implications of that in detail.

fedeazzato commented 8 years ago

Thanks for taking the time to analyze the issue and respond. I´m aware that VS uses msbuild itself behind the curtains. Is there a way for me to build my solutions mimicking how VS invokes msbuild? This is for my build script that I'm going to use in a CI server. That may be good enough for me until this is fixed. Thanks.

rainersigwald commented 8 years ago

I don't think there's a straightforward way to do that. VS invokes build for each project individually by logic that it controls. MSBuild attempts to replicate this logic when building a solution by generating .metaproj projects. Since this looks like a case where the logic isn't well-replicated, there's no easy way to get the .sln build working.

There are a few options that I can think of:

  1. Can you create a .proj file to build on your CI server, rather than the .sln? That keeps the logic of what to build entirely in MSBuild and is usually more understandable. But of course it can cause drift between what people do on their desktops (building with the .sln in VS) and what the "official" build does.
  2. You could try using a more invasive approach to avoid building the projects--for example by conditionally referencing the output binary as a <Reference>, rather than using ProjectReferences. That is doable but can be tricky to get right.
  3. You could configure your CI server to run devenv.exe /build instead of MSBuild directly. That should ensure that you're getting the VS logic when deciding what to build.
  4. Maybe you don't need this at all? You mentioned not wanting to build projects more than once, but MSBuild has built-in logic to avoid building a project more than once in a single MSBuild invocation. Can you change your build definition to build all of your solutions in one MSBuild call (maybe by creating a .proj that points to all of them)?
fedeazzato commented 8 years ago

Thanks for sharing your thoughts. It helped me understand a little more how things work under the hood.

I think I'm going to re-enable building theses projects (A.csproj in my sample) in the solutions that require them. I have already tried some of your suggestions. I'll share my experience bellow, just in case someone else finds this thread, and wants to know other users experience.

  1. This approach doesn't convince me. My team consists of 30 developers, and we've got around 10 solutions in our build script. If we have to maintain a custom proj synchronized with each solution, that would be a maintenance hell.
  2. We tried this for a while, and works great. The main drawback is not being able to see the source of A.dll when working in SolAandB.sln. When adding the reference I would have something like:

    <Reference Include="A">
     <HintPath>..\A\$(Configuration)\A.dll</HintPath>
     <Private>False</Private>
    </Reference>

    Note the hint path, that uses $(Configuration) to target the appropriate build output depending on the configuration. The only deal breaker of this approach, is that we have some custom build configurations (e.g. Release_Trial for a trial version), and not all projects are built in this configuration, but rather when building the solution in that configuration, the projects that don't need nothing special in Release_Trial are just built with the Release configuration, and so the hint path ..\A\Release_Trial\A.dll may not always be valid. VS and msbuild already solve this mess for us if we stay with <ProjectReference> and have the configuration mappings defined in the solution file.

  3. That's certainly valid. Not everyone is a fan of having VS installed on the build server, but other than that, I don't see any drawbacks.
  4. We already have a single proj file with all solutions listed, and use it to invoke msbuild with them. I used this approach in the sample project in BuildSolutions.proj. Please correct me if that's not the intended way to build the solutions from a unique proj file. The thing is that we have some C++ projects referencing C# projects, and the other way around too. The C++ projects with references to C#, are not so smart to not rebuild themselves, as they apparently have a difficult time deciding if the referenced C# project changed or not (I see warnings in output of some metadata cache failure), and that triggers the C++ project build again. And as a consequence of that, C# projects that reference those C++ projects are also built again.

In the long run, we´re probably going to transition to nuget dependencies, and instead of having a handful solutions with lots of projects, have lots of small solutions that build and publish nuget packages into a internal server, and other projects get the required packages from there. In the mean time, I'll re-enable building the disabled projects in the solution.

steve-torchia commented 7 years ago

Has there been any progress with this issue? I am seeing the same behavior using MSBuild 15.1.1012.6693

We are using build configurations to control the order of what we want built from out of our solution via the command line (i.e. abstractions before implementations) and the project references simply do not get built/included. I've even made sure that the project dependencies are properly configured in the sln file.

It was mentioned earlier in the thread that: BuildProjectInSolution="False" possibly was the culprit.

LeonardoX77 commented 7 years ago

Same issue for me, but solved checking build column of desired project in Configuration Manager.

renatogbp commented 6 years ago

I have similar issue, I want to set a Solution configuration which does not have a project configuration (I use only as a project filter), but the MSBuild does not recognize and ended up building all the projects. Right now, I am using devenv.exe to build the solution with the desired "filter".

chipplyman commented 5 years ago

I have the opposite problem: we want to enforce that all ProjectReferences are built as part of the solution, so we have implemented a target that enforces '%(ProjectReferenceWithConfiguration.BuildReference)' == 'true'.

MSBuild sets BuildProjectInSolution=false explicitly, which causes BuildReference to be set false and makes this validation logic work as expected. VS IDE leaves this property undefined, the AssignProjectConfiguration target in Microsoft.Common.CurrentVersion.Targets defaults an undefined BuildReference property to true, our validation encounters a false negative, and our build breaks with more esoteric errors from unresolved references in code.

We would prefer that MSBuild & VS IDE have matching behavior in that both should set BuildProjectInSolution=false on the ProjectConfiguration when a project is not configured to build in the current solution configuration.

Our solution to the problem of "I want project A to build as part of solution 1, then project B to build as part of solution 2" is for project B to use a Reference to A.dll instead of a ProjectReference to A.csproj. We still include project A in solution 2 for easy reference at coding time, but disable its Build in all solution configurations.