Home: Restore does not set SolutionDir properties for individual projects

Created on 29 Jan 2018  路  8Comments  路  Source: NuGet/Home

_From @couven92 on July 21, 2017 9:4_

I have one repository for my common .NET libraries that I use in multiple other projects. Other project repository include this common repository by using git submodules.

This leads to the following directory hierarchy:

MyProject\
  MyProject.sln
  Directory.Build.props
  src\MyProject\
    MyProject.csproj
  common\
    Common.sln
    Directory.Build.props
    src\Common\
      Common.csproj

MyProject.sln contains Project nodes for both MyProject.csproj and Common.csproj, while Common.sln naturally only contains a project node for Common.csproj

Contents of MyProject.csproj:

<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
  <ItemGroup>
    <ProjectReference Include="..\..\common\src\Common\Common.csproj" />
  </ItemGroup>
</Project>

I also like having my bin and obj folders in the Solution directory so both Directory.Build.props files define the BaseOutputPath and BaseIntermediateOutputPath properties like this:

<?xml version="1.0" encoding="utf-8"?>
<Project>
  <PropertyGroup Label="BasePath">
    <BaseOutputPath>$([MSBuild]::ValueOrDefault(`$(SolutionDir)`, `$(MSBuildThisFileDirectory)`))bin\</BaseOutputPath>
    <BaseIntermediateOutputPath>$([MSBuild]::ValueOrDefault(`$(SolutionDir)`, `$(MSBuildThisFileDirectory)`))obj\$(MSBuildProjectName)\</BaseIntermediateOutputPath>
  </PropertyGroup>
</Project>

When I open either solution in Visual Studio, restoring, building, debugging and running works perfectly fine with all intermediaries and binaries ending up where I expect them to be: in the bin and obj folders next to the Solution file I opened.

However, when I run dotnet restore MyProject.sln I can see that MSBuild restores both project, but places the intermediaries for Common.csproj into MyProject\common\obj and not into MyProject\obj as I'd expect.

Even worse, when I run dotnet build MyProject.sln, MSBuild rightfully complains that it cannot find the Assets file MyProject\obj\Common\project.assets.json as this file ended up in MyProject\common\obj\Common\project.assets.json when I ran dotnet restore MyProject.sln.

At a first glance, it seems like dotnet restore does not use the SolutionDir property which causes the paths in the BasePath property group in Directory.Build.props to use $(MSBuildThisFileDirectory) instead. This would actually be fine, but when running dotnet build it seems like the SolutionDir property actually IS set correctly.

I would expect the SolutionDir property ALWAYS being set when MSBuild interprets a solution file. In this case this is actually quite important, since there are multiple solution files in this directory structure.


.NET Core Information:

> dotnet --info
.NET Command Line Tools (2.0.0-preview2-006497)

Product Information:
 Version:            2.0.0-preview2-006497
 Commit SHA-1 hash:  06a2093335

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.15063
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\2.0.0-preview2-006497\

Microsoft .NET Core Shared Framework Host

  Version  : 2.0.0-preview2-25407-01
  Build    : 40c565230930ead58a50719c0ec799df77bddee9

MSBuild version:

> dotnet msbuild /version
Microsoft (R) Build Engine version 15.3.388.41745 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

15.3.388.41745

_Copied from original issue: Microsoft/msbuild#2342_

Restore 1 Bug

Most helpful comment

_From @cocowalla on December 4, 2017 14:43_

Ah, you're right: _Defined only when building a solution in the IDE_

I've worked around this by simply switching to $(ProjectDir) and moving up a dir with an extra ..\, e.g. $(ProjectDir)..\

All 8 comments

_From @cocowalla on December 4, 2017 10:28_

I'm hitting the same issue. Did you find any workaround for this?

_From @couven92 on December 4, 2017 14:20_

@cocowalla As far as I understand $(SolutionDir) is more a Visual Studio thing... So for MSBuild you should use a Directory.Build.props file next to your solution file and use $(MSBuildThisFileDirectory).

So if you want you could do this:

<?xml version="1.0" encoding="utf-8"?>
<Project>
  <PropertyGroup Label="BasePath">
    <SolutionDir Condition="'$(SolutionDir)'==''">$(MSBuildThisFileDirectory)</SolutionDir>
  </PropertyGroup>
</Project>

_From @cocowalla on December 4, 2017 14:43_

Ah, you're right: _Defined only when building a solution in the IDE_

I've worked around this by simply switching to $(ProjectDir) and moving up a dir with an extra ..\, e.g. $(ProjectDir)..\

_From @couven92 on December 4, 2017 15:26_

Well that would work og course, ny approach will work in any situation regardless og the number of levels between Solution and Project. And ProjectDir is theoretically only a little brother to SolutionDir meaning it's dependent on the tooling and may in theory be redefined or not be defined at all.

All these MSBuild properties are actual built-ins, you cannot change, redefine or overwrite them.

_From @nzain on January 29, 2018 9:34_

@couven92 thanks for the robust solution. We have several libraries that are pulled via svn:externals and all references start with $(SolutionDir)Externals to make this work in different directory structures. While in Visual Studio everything works, the msbuild scripts show warnings due to invalid paths (and actually do not restore !). Putting the Directory.Build.props file next to the .sln file resolves the problems between netcore, classic netframework libraries and msbuild. Thumbs up!

The Directory.Build.props file is an excellent, robust workaround. But

I would expect the SolutionDir property ALWAYS being set when MSBuild interprets a solution file. In this case this is actually quite important, since there are multiple solution files in this directory structure.

is absolutely reasonable.

Internally, MSBuild creates an in-memory project representation of a solution file and builds that. You can observe these by setting MSBUILDEMITSOLUTION=1 in your environment, which will write the generated solutions to disk. When doing so, we pass the solution directory down to each individual project. For example, the Build target in small solution I just created is implemented as:

<Target Name="Build" Outputs="@(CollectedBuildOutput)">
  <MSBuild Projects="@(ProjectReference)" BuildInParallel="True" Properties="BuildingSolutionFile=true; CurrentSolutionConfigurationContents=$(CurrentSolutionConfigurationContents); SolutionDir=$(SolutionDir); SolutionExt=$(SolutionExt); SolutionFileName=$(SolutionFileName); SolutionName=$(SolutionName); SolutionPath=$(SolutionPath)" SkipNonexistentProjects="%(ProjectReference.SkipNonexistentProjects)">
    <Output TaskParameter="TargetOutputs" ItemName="CollectedBuildOutput" />
  </MSBuild>
</Target>

However, the NuGet _GenerateRestoreGraph target, which collects information from each project, doesn't pass that:

<Target Name="_GenerateRestoreGraph" DependsOnTargets="_FilterRestoreGraphProjectInputItems;_GetAllRestoreProjectPathItems" Returns="@(_RestoreGraphEntry)">
  <Message Text="Generating dg file" Importance="low" />
  <Message Text="%(_RestoreProjectPathItems.Identity)" Importance="low" />
  <MsBuild BuildInParallel="$(RestoreBuildInParallel)" Projects="@(_GenerateRestoreGraphProjectEntryInput)" Targets="_GenerateRestoreGraphProjectEntry" Properties="$(_GenerateRestoreGraphProjectEntryInputProperties)">
    <Output TaskParameter="TargetOutputs" ItemName="_RestoreGraphEntry" />
  </MsBuild>
  <MsBuild BuildInParallel="$(RestoreBuildInParallel)" Projects="@(_RestoreProjectPathItems)" Targets="_GenerateProjectRestoreGraph" Properties="$(_GenerateRestoreGraphProjectEntryInputProperties)">
    <Output TaskParameter="TargetOutputs" ItemName="_RestoreGraphEntry" />
  </MsBuild>
</Target>

It should.

I can't think of a good way to enforce at the MSBuild layer that all MSBuild tasks in a solution metaproject carry the right metadata--especially since it would be legal to have an MSBuild task that did something else, and really doesn't want those properties.

So I'm going to move this issue to NuGet. The fix is to change

https://github.com/NuGet/NuGet.Client/blob/b82a7274e5f58c8af2eb052b8ff98d3d29fed66e/src/NuGet.Core/NuGet.Build.Tasks/NuGet.targets#L278-L288

to additionally pass (at least) SolutionDir.

@emgarten this seems like an easy enough fix, willing to consider for 4.7?

Let's put it in the backlog and prioritize it. This would be great to fix.

Was this page helpful?
0 / 5 - 0 ratings