Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setting BaseIntermediateOutputPath correctly in a SDK-based project is hard #1603

Closed
srivatsn opened this issue Jan 24, 2017 · 36 comments
Closed
Labels

Comments

@srivatsn
Copy link
Contributor

srivatsn commented Jan 24, 2017

NuGet restore drops the project.assets.json file to the $(BaseIntermediateOutpath). If a user customizes that by setting the property in a SDK-based project like so:

<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
         <BaseIntermediateOutputPath>C:\blah</BaseIntermediateOutputPath>
   </PropertyGroup>
</Project>

Then the project.assets.json gets dropped to that folder correctly. However if there are nuget packages in this project that have tasks\targets then the generated project.nuget.g.props\targets are imported by Microsoft.Common.Props. Therefore BaseIntermediateOutputPath needs to be defined before the import of the common props and for that they have to know to expand the SDK import:

<Project>
   <PropertyGroup>
         <BaseIntermediateOutputPath>C:\blah</BaseIntermediateOutputPath>
   </PropertyGroup>
   <Import Sdk="Microsoft.NET.Sdk" Project="Sdk.props" />
...
   <Import Sdk="Microsoft.NET.Sdk" Project="Sdk.targets" />
</Project>

I don't know what we can do to fix this but logging an issue here so that atleast this serves as documentation for people running into this issue.

@cdmihai
Copy link
Contributor

cdmihai commented Jan 26, 2017

An easier way is to use the new Sdk attribute for import elements:

<Project>
  <PropertyGroup>
    <BaseIntermediateOutputPath>C:\blah</BaseIntermediateOutputPath>
  </PropertyGroup>

  <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk/1.0.0" />
...
  <Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk/1.0.0" />
</Project>

See #1493

@srivatsn
Copy link
Contributor Author

Has that syntax been implemented already?

@cdmihai
Copy link
Contributor

cdmihai commented Jan 26, 2017

According to this diff it was: https://github.com/Microsoft/msbuild/pull/1492/files#diff-325c8a74f9ae27c1b3f8870e9cb64678L2310

@rainersigwald
Copy link
Member

For posterity: another option is using a Directory.Build.props to set this. See dotnet/sdk#802.

@Ziflin
Copy link

Ziflin commented Feb 28, 2017

Is there are "correct" solution for this? All I'm trying to do is move the bin\ and obj\ folders that .NET Core projects dump in the project folder into a Build\ directory at the solution level so they're separate from the source code. I added this:

    <PropertyGroup>
        <OutputPath>$(SolutionDir)\Build\$(ProjectName)\bin\$(Configuration)</OutputPath>
        <BaseIntermediateOutputPath>$(SolutionDir)\Build\$(ProjectName)\obj</BaseIntermediateOutputPath>
    </PropertyGroup>

to the top of our .csproj files, but it appears that $(ProjectName) is not set at this point? Is there some correct way to accomplish this as it's driving me crazy. :) Note that it appears that $(SolutionDir) works fine. It also looks like OutputPath always has the netcoreapp1.1 or netstandard1.6.1 folder appended as well.

@rainersigwald
Copy link
Member

Both moving the imports as in #1603 (comment) and using a Directory.Build.props as in #1603 (comment) are "correct", @Ziflin. Chose whichever meets your needs better.

@Ziflin
Copy link

Ziflin commented Feb 28, 2017

I don't have an imports section. This is just a simple .NET Core Console App and Lib solution. So the console app uses:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>netcoreapp1.1</TargetFramework>
    </PropertyGroup>
</Project>

And I added the PropertyGroup in my first comment above this PropertyGroup. Is $(ProjectName) supposed to be valid or is there some other variable name I can use that is for the project's name? It's basically treating it like it's not set.

Sorry I'm not clear on how to use the Directory.Build.props from that other comment/issue..

@rainersigwald
Copy link
Member

@Ziflin your two options are:

Create a file named Directory.Build.props in a folder above your projects (maybe next to your solution? you know your repo layout best) that sets the properties you want to set. It will be automatically included in any project below it in the directory structure.

Or change your project file from the implicit imports model to explicit imports, so that you can control order. These are exactly identical:

-<Project Sdk="Microsoft.NET.Sdk">
+<Project>
+ <Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>netcoreapp1.1</TargetFramework>
    </PropertyGroup>
+ <Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
</Project>

After you've made the implicit imports explicit, you can add or move things around them to affect relative order.

@Ziflin
Copy link

Ziflin commented Feb 28, 2017

@rainersigwald Thanks for the help, but neither of those methods seem to have any effect on the fact that the $(ProjectName) variable is not set which makes it very difficult to do something like:

<BaseIntermediateOutputPath>$(SolutionDir)\Build\$(ProjectName)\obj</BaseIntermediateOutputPath>

And have it create a Build\MyProject\obj folder under the solution. If I hardcode a name for $(ProjectName):

<BaseIntermediateOutputPath>$(SolutionDir)\Build\MyProject\obj</BaseIntermediateOutputPath>

then it works as expected, but then I am unable to use a single Directory.Build.props file for all the projects in the solution. The $(SolutionDir) and $(Configuration) variables seem to work fine so I wasn't sure if I just had the name wrong for the project name variable or if it was a bug.

@rainersigwald
Copy link
Member

Would $(MSBuildProjectName), which is a well-known property automatically populated by MSBuild based on the file name, work for you?

@Ziflin
Copy link

Ziflin commented Feb 28, 2017

That works perfectly! Sorry for the confusion. I saw that $(SolutionDir) was working and assumed $(ProjectName) would as well.

@Nirmal4G
Copy link
Contributor

Nirmal4G commented Sep 20, 2017

From @srivatsn
BaseIntermediateOutputPath needs to be defined before the import of the common props and for that they have to know to expand the SDK import.

Even easier way is to generalize implicit imports placement.

<Project Sdk="Custom.Sdk">
  <PropertyGroup Evaluation="BeforeImplicitProps">
    <BaseIntermediateOutputPath>..\..\Build</BaseIntermediateOutputPath>
  </PropertyGroup>
...
  <PropertyGroup Evaluation="AfterImplicitTargets">
    <SomeImportantPropertyAfterTargets>Value!!!</SomeImportantPropertyAfterTargets>
  </PropertyGroup>
</Project>

would translate to this

<Project>
  <PropertyGroup>
    <BaseIntermediateOutputPath>..\..\Build</BaseIntermediateOutputPath>
  </PropertyGroup>

  <Import Project="Sdk.props" Sdk="Custom.Sdk/1.0.0" />
...
  <Import Project="Sdk.targets" Sdk="Custom.Sdk/1.0.0" />

  <PropertyGroup>
    <SomeImportantPropertyAfterTargets>Value!!!</SomeImportantPropertyAfterTargets>
  </PropertyGroup>
</Project>

Here, assume Custom Sdk uses common Sdk. This is helpful in creating custom .proj file with Sdk story to them like I can use any custom Sdk that uses common Sdk or itself. See Issue #1686

This is one way to fix the problem and It does it even before all the props and after all the targets, which would be useful for many debugging and logging scenarios.

@jeffkl
Copy link
Contributor

jeffkl commented Sep 20, 2017

@Nirmal4G Directory.Build.props is imported before BaseIntermediateOutputPath is set so I would recommend you just use that.

@Nirmal4G
Copy link
Contributor

Nirmal4G commented Sep 20, 2017

@jeffkl I know!

But I want to set some properties (within the Project file) even before all the implicit props and have some targets after all the implicit targets, something along those lines!

@KevinH-MS
Copy link

Where in the SDK props do we implicitly import .props from nuget packages? I wanted to look at it and see if I can come up with any other clever tricks for my Sdk props (trying to avoid Directory.Build.props for the time being), but I can't find the actual place where we look at the project assets and import them.

@Nirmal4G
Copy link
Contributor

Nirmal4G commented Nov 1, 2017

See the lines here, the comments will tell you everything!

<!--
Determine the path to the directory build props file if the user did not disable $(ImportDirectoryBuildProps) and
they did not already specify an absolute path to use via $(DirectoryBuildPropsPath)
-->
<PropertyGroup Condition="'$(ImportDirectoryBuildProps)' == 'true' and '$(DirectoryBuildPropsPath)' == ''">
<_DirectoryBuildPropsFile Condition="'$(_DirectoryBuildPropsFile)' == ''">Directory.Build.props</_DirectoryBuildPropsFile>
<_DirectoryBuildPropsBasePath Condition="'$(_DirectoryBuildPropsBasePath)' == ''">$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), '$(_DirectoryBuildPropsFile)'))</_DirectoryBuildPropsBasePath>
<DirectoryBuildPropsPath Condition="'$(_DirectoryBuildPropsBasePath)' != '' and '$(_DirectoryBuildPropsFile)' != ''">$([System.IO.Path]::Combine('$(_DirectoryBuildPropsBasePath)', '$(_DirectoryBuildPropsFile)'))</DirectoryBuildPropsPath>
</PropertyGroup>
<PropertyGroup Condition="'$(ImportDirectoryBuildProps)' == 'true' and exists('$(DirectoryBuildPropsPath)')">
<MSBuildAllProjects>$(MSBuildAllProjects);$(DirectoryBuildPropsPath)</MSBuildAllProjects>
</PropertyGroup>
<Import Project="$(DirectoryBuildPropsPath)" Condition="'$(ImportDirectoryBuildProps)' == 'true' and exists('$(DirectoryBuildPropsPath)')"/>
<!--
Prepare to import project extensions which usually come from packages. Package management systems will create a file at:
$(MSBuildProjectExtensionsPath)\$(MSBuildProjectFile).<SomethingUnique>.props
Each package management system should use a unique moniker to avoid collisions. It is a wild-card import so the package
management system can write out multiple files but the order of the import is alphabetic because MSBuild sorts the list.
-->
<PropertyGroup>
<!--
The declaration of $(BaseIntermediateOutputPath) had to be moved up from Microsoft.Common.CurrentVersion.targets
in order for the $(MSBuildProjectExtensionsPath) to use it as a default.
-->
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">obj\</BaseIntermediateOutputPath>
<BaseIntermediateOutputPath Condition="!HasTrailingSlash('$(BaseIntermediateOutputPath)')">$(BaseIntermediateOutputPath)\</BaseIntermediateOutputPath>
<MSBuildProjectExtensionsPath Condition="'$(MSBuildProjectExtensionsPath)' == '' ">$(BaseIntermediateOutputPath)</MSBuildProjectExtensionsPath>
<!--
Import paths that are relative default to be relative to the importing file. However, since MSBuildExtensionsPath
defaults to BaseIntermediateOutputPath we expect it to be relative to the project directory. So if the path is relative
it needs to be made absolute based on the project directory.
-->
<MSBuildProjectExtensionsPath Condition="'$([System.IO.Path]::IsPathRooted($(MSBuildProjectExtensionsPath)))' == 'false'">$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(MSBuildProjectExtensionsPath)'))</MSBuildProjectExtensionsPath>
<MSBuildProjectExtensionsPath Condition="!HasTrailingSlash('$(MSBuildProjectExtensionsPath)')">$(MSBuildProjectExtensionsPath)\</MSBuildProjectExtensionsPath>
<ImportProjectExtensionProps Condition="'$(ImportProjectExtensionProps)' == ''">true</ImportProjectExtensionProps>
</PropertyGroup>
<Import Project="$(MSBuildProjectExtensionsPath)$(MSBuildProjectFile).*.props" Condition="'$(ImportProjectExtensionProps)' == 'true' and exists('$(MSBuildProjectExtensionsPath)')" />

And in the targets...

<!--
Prepare to import project extensions which usually come from packages. Package management systems will create a file at:
$(MSBuildProjectExtensionsPath)\$(MSBuildProjectFile).<SomethingUnique>.targets
Each package management system should use a unique moniker to avoid collisions. It is a wild-card import so the package
management system can write out multiple files but the order of the import is alphabetic because MSBuild sorts the list.
-->
<PropertyGroup>
<ImportProjectExtensionTargets Condition="'$(ImportProjectExtensionTargets)' == ''">true</ImportProjectExtensionTargets>
</PropertyGroup>
<Import Project="$(MSBuildProjectExtensionsPath)$(MSBuildProjectFile).*.targets" Condition="'$(ImportProjectExtensionTargets)' == 'true' and exists('$(MSBuildProjectExtensionsPath)')" />
<PropertyGroup>
<ImportDirectoryBuildTargets Condition="'$(ImportDirectoryBuildTargets)' == ''">true</ImportDirectoryBuildTargets>
</PropertyGroup>
<!--
Determine the path to the directory build targets file if the user did not disable $(ImportDirectoryBuildTargets) and
they did not already specify an absolute path to use via $(DirectoryBuildTargetsPath)
-->
<PropertyGroup Condition="'$(ImportDirectoryBuildTargets)' == 'true' and '$(DirectoryBuildTargetsPath)' == ''">
<_DirectoryBuildTargetsFile Condition="'$(_DirectoryBuildTargetsFile)' == ''">Directory.Build.targets</_DirectoryBuildTargetsFile>
<_DirectoryBuildTargetsBasePath Condition="'$(_DirectoryBuildTargetsBasePath)' == ''">$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), '$(_DirectoryBuildTargetsFile)'))</_DirectoryBuildTargetsBasePath>
<DirectoryBuildTargetsPath Condition="'$(_DirectoryBuildTargetsBasePath)' != '' and '$(_DirectoryBuildTargetsFile)' != ''">$([System.IO.Path]::Combine('$(_DirectoryBuildTargetsBasePath)', '$(_DirectoryBuildTargetsFile)'))</DirectoryBuildTargetsPath>
</PropertyGroup>
<PropertyGroup Condition="'$(ImportDirectoryBuildTargets)' == 'true' and exists('$(DirectoryBuildTargetsPath)')">
<MSBuildAllProjects>$(MSBuildAllProjects);$(DirectoryBuildTargetsPath)</MSBuildAllProjects>
</PropertyGroup>
<Import Project="$(DirectoryBuildTargetsPath)" Condition="'$(ImportDirectoryBuildTargets)' == 'true' and exists('$(DirectoryBuildTargetsPath)')"/>

That is how nuget and other package managers (paket, etc) import the restored assets!

@Nirmal4G
Copy link
Contributor

Nirmal4G commented Nov 1, 2017

You can modify MSBuildProjectExtensionsPath to generalize your assets output!

@dasMulli
Copy link
Contributor

dasMulli commented Nov 1, 2017

It's not just MSBuildProjectExtensionsPath though, as I wrote for https://stackoverflow.com/questions/45575280/msbuild-nuget-restoreoutputpath-how-to-make-it-work, there are more properties that need to work together:

  • BaseIntermediateOutputPath - used to construct:
    • ProjectAssetsFile (SDK targets)
    • MSBuildProjectExtensionsPath if unset (MSBuild - Microsoft.Common.props)
    • RestoreOutputPath (NuGet targets)
  • MSBuildProjectExtensionsPath - could theoretically be set to something different
  • RestoreOutputPath - tells restore where to drop assets file and extension targets.

If those three don't point to the same directory -> 💩

The dangerous part is that those three properties are coming from different components so if you're not careful about which one is set where (=> set base..path early or set all), you won't have a good time.

@Nirmal4G
Copy link
Contributor

Nirmal4G commented Nov 1, 2017

I had the same experience, and that was the solution that saved me, thank you for that, but if you are using the latest tools you can get away with those problems.

But if you are using props/targets that are before the ProjectExtensions logic, It's better to shim up (by detecting ImportProjectExtensionProps) those in Directory.Build.props/targets so that you can use them with your old toolsets ensuring forward compatibility.

@dsplaisted
Copy link
Member

This was fixed with NuGet/NuGet.Client#2131 and #3059

@gaziqbal
Copy link

gaziqbal commented Feb 5, 2019

To summarize, if I want to override my intermediate and output folders, it appears that following is needed in a Directory.Build.props file in my project or enclosing solution folder. Please call out if this is incorrect :)

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <!-- Common properties -->
  <PropertyGroup>
    <!-- SolutionDir is not defined when building projects explicitly -->
    <SolutionDir Condition=" '$(SolutionDir)' == '' ">$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), MySolution.sln))\</SolutionDir>
    <!-- Output paths -->
    <BaseIntermediateOutputPath>$(SolutionDir)bin\obj\$(Configuration)\$(MSBuildProjectName)\</BaseIntermediateOutputPath>
    <IntermediateOutputPath>$(SolutionDir)bin\obj\$(Configuration)\$(MSBuildProjectName)\</IntermediateOutputPath>
    <MSBuildProjectExtensionsPath>$(IntermediateOutputPath)\</MSBuildProjectExtensionsPath>
    <OutputPath>$(SolutionDir)bin\out\$(Configuration)\</OutputPath>
    <OutDir>$(OutputPath)</OutDir>
    <DocumentationFile>$(SolutionDir)bin\doc\$(Configuration)\$(MSBuildProjectName).xml</DocumentationFile>
  </PropertyGroup>
</Project>

fibann added a commit to microsoft/MixedReality-Sharing that referenced this issue Jun 6, 2019
@rollsch
Copy link

rollsch commented Nov 15, 2019

After reading this whole thread I cannot believe there is not a better solution. There should be a simple "ObjectPath" and "BinPath" in the project properties that can be filled out via the VS GUI.

It is such a simple request to move the intermediate directories. Instead we need a separate file? It has taken me an hour of trying things and reading this thread to figure out how to do such a "simple" task.

@dasMulli
Copy link
Contributor

I feel your pain. Unfortunately that's the downside of making the common way super easy and clean - configuring things deep down get harder. mostly duet to history and the way things are layered.

@agaace
Copy link

agaace commented Nov 18, 2019

"configuring things deep down get harder. mostly duet to history and the way things are layered"

Sorry but I don't consider changing the output directory for a project a "deep down" configuration.
"mostly duet to history and the way things are layered" is also not an acceptable excuse for me in a brand new framework. I mean, .NET has been around for nearly 2 decades, fine, it has acquired its quirks over time. But that's why .Core was not a version upgrade and an entirely new framework requiring migration - fine by me. And now you're brushing issues off by saying .Core has its "history baggage"? Sorry, not buying it.

Sorry for yet another rant. I just wanted to build a simple .Core project and been trying to figure this issue out since yesterday. Could have literally written hundreds of lines in the meantime but I had to spend all this time reading threads, jumping to links, reading documentation and trying to put it all together just to figure out how to configure my build. Turns out you either let Visual Studio generate everything for you, or you need a PhD in how MS Build works internally. There's nothing, null, nada, zero in between. OK, rant over.

@Nirmal4G
Copy link
Contributor

@agaace You are absolutely right.

I myself from .NET framework days, and I still want many changes that are proposed here.

As long as there are docs documenting the migration for every new breaking change, I don't care about how many are there.

Forgive me, team, but what he said, I 💯 agree.

stkb added a commit to stkb/Rewrap that referenced this issue Mar 21, 2021
Makes things a little bit cleaner.

Needed to modify the two project files to have different /obj folders,
otherwise there are conflicts. Reference
dotnet/msbuild#1603 (comment)
@Limezero
Copy link

Has there been any progress on this? As far as I can tell, .NET 8 still requires you to use either the awkward separate file (which requires you to version control and keep track of yet more things, and doesn't let you set directories per-project) or the similarly obtuse <Import Sdk=.../> pair. Even worse, naively adding <BaseOutputPath> and <BaseIntermediateOutputPath> to your project works perfectly fine without an error, until you find out that some things weren't overridden properly, and you spend an hour tearing your hair out and tracking down this GitHub issue like I just did.

MSBuild needs a way to set the intermediary directories of a project with a single, simple property in your .csproj file that functions as expected. Period, end of story, I don't care what needs to change to make that happen.

@mvonballmo
Copy link

mvonballmo commented Jan 19, 2024

To summarize, if I want to override my intermediate and output folders, it appears that following is needed in a Directory.Build.props file in my project or enclosing solution folder. Please call out if this is incorrect :)

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <!-- Common properties -->
  <PropertyGroup>
    <!-- SolutionDir is not defined when building projects explicitly -->
    <SolutionDir Condition=" '$(SolutionDir)' == '' ">$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), MySolution.sln))\</SolutionDir>
    <!-- Output paths -->
    <BaseIntermediateOutputPath>$(SolutionDir)bin\obj\$(Configuration)\$(MSBuildProjectName)\</BaseIntermediateOutputPath>
    <IntermediateOutputPath>$(SolutionDir)bin\obj\$(Configuration)\$(MSBuildProjectName)\</IntermediateOutputPath>
    <MSBuildProjectExtensionsPath>$(IntermediateOutputPath)\</MSBuildProjectExtensionsPath>
    <OutputPath>$(SolutionDir)bin\out\$(Configuration)\</OutputPath>
    <OutDir>$(OutputPath)</OutDir>
    <DocumentationFile>$(SolutionDir)bin\doc\$(Configuration)\$(MSBuildProjectName).xml</DocumentationFile>
  </PropertyGroup>
</Project>

I'm here in 2023 and just wanted to extend this answer, because it's quite tempting to use as-is, but I don't think all of it is needed anymore. I wanted to move the bin/ and obj/ folders outside of Source/

Source/
Output/
  bin/
    proj1/
    ...
  obj/
    proj1/
    ...

There may be more efficient ways of compiling i.e. combining library output folders so avoid regenerating libs, but I've not gotten that far yet. I also have some older WPF projects in there that I'm more careful with.

I've gotten the effect I wanted with the following configuration in a Directory.Build.Props file at the solution-file level:

<OutputPath>$(SolutionDir)../Output/bin/$(MSBuildProjectName)</OutputPath>
<IntermediateOutputPath>$(SolutionDir)../Output/obj/$(MSBuildProjectName)</IntermediateOutputPath>
<BaseIntermediateOutputPath>$(IntermediateOutputPath)</BaseIntermediateOutputPath>

The documentation destination doesn't have to be explicit anymore. Instead, you can use GenerateDocumentationFile as follows:

<GenerateDocumentationFile>true</GenerateDocumentationFile>

With those directives, I no longer have any obj/ and bin/ folders in Source/ and everything restores and compiles reliably.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests