dotnet / sdk

Core functionality needed to create .NET Core projects, that is shared between Visual Studio and CLI
https://dot.net/core
MIT License
2.68k stars 1.06k forks source link

Setting BaseIntermediateOutputPath breaks netstandard2.0 builds #2003

Open DoCode opened 6 years ago

DoCode commented 6 years ago

Because:

When I use a netcoreapp2.x with a project reference to a netstandard2.x project and I set the BaseIntermediateOutputPath, then the build/publish failed with this error:

~\Microsoft.NET.Sdk\build\Microsoft.PackageDependencyResolution.targets(201,5): error :
Assets file '~\netcoreapp-netstandard-refs\src\_build\bin\obj\project.assets.json' doesn't have a target for '.NETStandard,Version=v2.0'.
Ensure that restore has run and that you have included 'netstandard2.0' in the TargetFrameworks for your project.
[~\netcoreapp-netstandard-refs\src\dotnetcore-lib\dotnetcore-lib.csproj]

Steps to reproduce:

1) Create class lib dotnet new classlib --name dotnetcore-lib --output src/dotnetcore-lib 2) Create web app dotnet new web --name aspnetcore --output src/aspnetcore 3) Add class lib as reference to web app dotnet add .\src\aspnetcore\aspnetcore.csproj reference .\src\dotnetcore-lib\dotnetcore-lib.csproj 4) Restore and publish (without custom BaseIntermediateOutputPath) => that works 👍

    dotnet restore .\src\dotnetcore-lib\dotnetcore-lib.csproj
    dotnet restore .\src\aspnetcore\aspnetcore.csproj
    dotnet publish .\src\dotnetcore-lib\dotnetcore-lib.csproj
    dotnet publish .\src\aspnetcore\aspnetcore.csproj

5) Restore and publish (with custom BaseIntermediateOutputPath) => that fails 👎

    dotnet restore .\src\dotnetcore-lib\dotnetcore-lib.csproj /p:BaseIntermediateOutputPath=..\..\_build\bin\obj\dotnetcore-lib\
    dotnet publish .\src\dotnetcore-lib\dotnetcore-lib.csproj /p:BaseIntermediateOutputPath=..\..\_build\bin\obj\dotnetcore-lib\
    dotnet restore .\src\aspnetcore\aspnetcore.csproj /p:BaseIntermediateOutputPath=..\..\_build\bin\obj\aspnetcore\
    dotnet publish .\src\aspnetcore\aspnetcore.csproj /p:BaseIntermediateOutputPath=..\..\_build\bin\obj\aspnetcore\

What the hell we make it wrong?

Related issues

livarcocc commented 6 years ago

@nguerrera

livarcocc commented 6 years ago

cc @dsplaisted can you take a look at this one? I remember you were looking at something similar already.

dsplaisted commented 6 years ago

Setting BaseIntermediateOutputPath from the command line isn't supported when you have project references.

The problem is, when you build a project, its references are also built. In this case, when you build aspnetcore.csproj, it in turn builds dotnetcore-lib.csproj, and the BaseIntermediateOutputPath flows through and both projects end up trying to use the same project.assets.json file, which causes this error (a project ends up trying to use the assets file which was written for another project).

If we were to change it so that the BaseIntermediateOutputPath didn't flow to project references, then when aspnetcore.csproj builds dotnetcore-lib.csproj, the BaseIntermediateOutputPath wouldn't be specified at all, so it would put the intermediate output in the default folder (obj in the project directory) instead of the _build folder you specified when you built that project previously.

Instead of trying to specify this on the command line, I'd suggest creating a Directory.Build.props file in the root folder with the following contents (or similar):

<Project>
  <PropertyGroup>
    <BaseIntermediateOutputPath>$(MSBuildThisFileDirectory)_build\bin\obj\$(MSBuildProjectName)\</BaseIntermediateOutputPath>
  </PropertyGroup>
</Project>

See also #867 which proposes adding a property to specify a root path under which all project output (intermediate as well as final) would go. If we add such a property, then it should be possible to specify it from the command line.

DoCode commented 6 years ago

First, @livarcocc, @nguerrera... Thanks for your fast response! Very Great!

@dsplaisted, ok. I understand and tried it. And it worked!

But what about when I restore, build/publish with --no-dependencies --force like this:

dotnet restore .\src\dotnetcore-lib\dotnetcore-lib.csproj /p:BaseIntermediateOutputPath=..\..\_build\bin\obj\dotnetcore-lib\ --no-dependencies --force
dotnet publish .\src\dotnetcore-lib\dotnetcore-lib.csproj /p:BaseIntermediateOutputPath=..\..\_build\bin\obj\dotnetcore-lib\ --no-dependencies --force
dotnet restore .\src\aspnetcore\aspnetcore.csproj /p:BaseIntermediateOutputPath=..\..\_build\bin\obj\aspnetcore\ --no-dependencies --force
dotnet publish .\src\aspnetcore\aspnetcore.csproj /p:BaseIntermediateOutputPath=..\..\_build\bin\obj\aspnetcore\ --no-dependencies --force

This does not work. But it from the wording from the arguments it should.

The question is, how can we tell the underlying compiler/linker the path to the 'pre-compiled' dotnetcore-lib.csproj obj dir?

dsplaisted commented 6 years ago

how can we tell the underlying compiler/linker the path to the 'pre-compiled' dotnetcore-lib.csproj obj dir?

If you want to use a DLL directly instead of building the referenced project, then you can use a Reference instead of a ProjectReference in your project file. But I think it would be helpful to understand why you want to do this, there may be a better way to do it.

As for why --no-dependencies didn't work, even if the referenced project isn't being built, it still calls the GetTargetPath target on it in order to figure out what the path to the DLL to reference is.

DoCode commented 6 years ago

@dsplaisted thanks for your support. The only simple answer to your question is, that we would redirect the base obj (BIOP) folder outside of the project directory structure. Is there a solution for this?

dsplaisted commented 6 years ago

@DoCode I'm not following. If you want to redirect the obj folder, then use a Directory.Build.props file like I suggested. The path you put in that file can be outside the project directory structure if you want it.

You probably don't need to use the --no-dependencies or --force options.

DoCode commented 6 years ago

@dsplaisted thanks again! I am a little bit overcommited ;-) So what I see today, I can not build dynamically a output folder structure like this:

_build
|-> bin
    |-> anyos.anycpu.<configuration>
        |-> <project-name>
            |-> <target-framework>
                .
                .
                .
    |-> win7.x86.debug
        |-> Project1
            |-> netstandard2.0
                .
                .
                .

The only problem is, that I can not dynamic parse the informations like cpu, architecture, target framework and so on, when I use the directory.build.props.

dsplaisted commented 6 years ago

@DoCode Yes, the target framework and runtime identifier aren't available at that point. By default they are appended to the path under the project-specific path. So if you set BaseIntermediateOutputPath to build\obj\<project-name>, then you will end up with folders such as Debug\netstandard2.0\win7-x86 under it.

DoCode commented 6 years ago

@dsplaisted ok. No chance to calculate and read the props in a custom task (default task in directory.build.props)?

dsplaisted commented 6 years ago

@DoCode No, these properties are set in evaluation, which is before any tasks are run.

It's probably possible to get close to the layout you want, but it would be a lot trickier. You'd still set the BaseIntermediateOutputPath to something like build\<project-name>\obj in Directory.Build.props, but then you would calculate the IntermediateOutputPath and OutputPath in a .targets file that was evaluated after the TargetFramework and other properties you need were set.

You can use binary logs and MSBuild Structured Log Viewer to explore how the current output path calculations work, and help figure out how to override them with your own. Look for the AppendTargetFrameworkToOutputPath and AppendRuntimeIdentifierToOutputPath properties.

DoCode commented 6 years ago

Thanks a lot, Daniel (@dsplaisted)! I will test this tomorrow... Now it's very late in the Bavarian Alps! :-)

DoCode commented 6 years ago

@dsplaisted - sorry for the long delay. But other tasks have higher prio!

I ended with this Directory.Build.props:

<?xml version="1.0" encoding="utf-8"?>
<Project>

  <PropertyGroup>
    <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
  </PropertyGroup>

  <Import Project="../Directory.Build.props" Condition=" Exists('../Directory.Build.props') " />

  <Import Project="../dir.props" Condition=" Exists('../dir.props') " />
  <Import Project="SolutionItems/version/version.props" Condition=" Exists('SolutionItems/version/version.props') " />

  <PropertyGroup Condition=" '$(ConfigPropsFileImported)' == 'true' ">
    <!--
      _build / bin /
    -->
    <BaseOutputPath>$(BinDir)/</BaseOutputPath>

    <!--
      _build / obj /
    -->
    <BaseIntermediateOutputPath>$(BaseOutputDir)/obj/$(MSBuildProjectName)/</BaseIntermediateOutputPath>

    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
    <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
  </PropertyGroup>

</Project>

And this Directory.Build.targets:

<?xml version="1.0" encoding="utf-8"?>
<Project>

  <PropertyGroup>
    <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
  </PropertyGroup>

  <Import Project="../Directory.Build.targets" Condition=" Exists('../Directory.Build.targets') " />

  <Import Project="../dir.targets" Condition=" Exists('../dir.targets') " />

  <PropertyGroup Condition=" '$(ConfigPropsFileImported)' == 'true' ">
    <!--
      _build / bin / win7.x64.debug / DotNetCore.ConsoleApp / netcoreapp2.1 /
      _build / bin / $(_OS).$(_PlatformTarget).$(Configuration) / $(MSBuildProjectName) / $(_TargetFramework) /
    -->

    <_OS Condition=" '$(RuntimeIdentifier)' != '' ">$(RuntimeIdentifier.Split('-')[0])</_OS>
    <_OS Condition=" '$(_OS)' == '' ">AnyOS</_OS>
    <_PlatformTarget>$(PlatformTarget)</_PlatformTarget>
    <_PlatformTarget Condition=" '$(_PlatformTarget)' == '' ">AnyCPU</_PlatformTarget>
    <_TargetFramework>$(TargetFramework)</_TargetFramework>
    <_TargetFramework Condition=" '$(_TargetFramework)' == '' AND '$(TargetFrameworkIdentifier)' == '.NETFramework' ">net$(_TargetFrameworkVersionWithoutV.Replace('.', ''))</_TargetFramework>

    <IntermediateOutputPath>$(BaseIntermediateOutputPath)$(_OS.ToLowerInvariant()).$(_PlatformTarget.ToLowerInvariant()).$(Configuration.ToLowerInvariant())/$(_TargetFramework.ToLowerInvariant())/</IntermediateOutputPath>
    <IntermediateOutputPath>$(IntermediateOutputPath.TrimEnd('/').TrimEnd('\'))/</IntermediateOutputPath>

    <OutputPath>$(BinDir)/$(_OS.ToLowerInvariant()).$(_PlatformTarget.ToLowerInvariant()).$(Configuration.ToLowerInvariant())/$(MSBuildProjectName)/$(_TargetFramework.ToLowerInvariant())/</OutputPath>
    <OutputPath>$(OutputPath.TrimEnd('/').TrimEnd('\'))/</OutputPath>

    <OutDir>$(OutputPath)</OutDir>
    <BaseOutputPath>$(OutputPath)</BaseOutputPath>
    <PackageOutputPath>$(OutputPath)</PackageOutputPath>
    <TargetDir>$(OutputPath)</TargetDir>
    <TargetPath>$(TargetDir)$(TargetFileName)</TargetPath>
    <PublishDir>$(OutputPath)</PublishDir>
  </PropertyGroup>

</Project>

Results

Now some errors occours on build:

    Error MSB4018: The "GenerateBindingRedirects" task failed unexpectedly.
System.IO.DirectoryNotFoundException: Could not find a part of the path 'D:\projects\output-labs\_build\obj\DotNetCore.ConsoleApp\Debug\repocli.exe.config'.

I think the GenerateBindingRedirects task doesnt respect the changed IntermediateOutputPath.

IngmarBitter commented 3 years ago

This issue still presists:

in my net50 C# solution building with VS2019 I get no warnings with

<BaseArtifactsPath>$(MSBuildStartupDirectory)artifacts/</BaseArtifactsPath>
<BaseArtifactsPathSuffix>$(ProjectCategory)/$(MSBuildProjectName)</BaseArtifactsPathSuffix>

and Unknown build errors that disappear at the end of compilation, if I add the line

<BaseIntermediateOutputPath>$(BaseArtifactsPath)obj/$(BaseArtifactsPathSuffix)/</BaseIntermediateOutputPath>

but even with this last line a startupDir/obj directory is still created with Debug/net5.0-windows subdirectory. And there is no obj directory created inside startupDir/artifacts. So the intended effect is not there.