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

Support setting a root output path for all projects in a repo or solution with a single property #3497

Closed
dsplaisted opened this issue Jul 9, 2018 · 21 comments
Labels
Milestone

Comments

@dsplaisted
Copy link
Member

Currently there are several different properties that control where project output goes. If you want all project output for a repo to go under a single folder, it is certainly possible to set this up, but it involves setting several properties, and knowing how to include $(MSBuildProjectName) as part of the path. For example, you can put the following in a Directory.Build.props file (assuming all your projects use Microsoft.NET.Sdk):

<RepoRoot>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))</RepoRoot>
<BaseOutputPath>$(RepoRoot)artifacts\bin\$(MSBuildProjectName)\</BaseOutputPath>
<BaseIntermediateOutputPath>$(RepoRoot)artifacts\obj\$(MSBuildProjectName)\</BaseIntermediateOutputPath>

We would like to add a single property (for example RootOutputPath) that handles this automatically.

See also dotnet/sdk#867

@dsplaisted
Copy link
Member Author

Straw man:

  • OutputPath: <RootOutputPath>\bin\<MSBuildProjectName>\<Configuration>\[Platform]\[TargetFramework]\[RuntimeIdentifier]
  • IntermediateOutputPath: <RootOutputPath>\obj\<MSBuildProjectName>\<Configuration>\[Platform]\[TargetFramework]\[RuntimeIdentifier]
  • PackageOutputPath: <RootOutputPath>\packages\<Configuration>

@KirillOsenkov
Copy link
Member

I found that I also need to default the $(Configuration) to Debug otherwise it will misplace the obj after a git clean. Here's what I arrived at:

Directory.Build.props

<Project>

  <PropertyGroup>
    <Configuration Condition="$(Configuration) == ''">Debug</Configuration>
    <RepoRoot>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)\..\..\'))</RepoRoot>
    <BaseOutputPath>$(RepoRoot)bin\$(Configuration)\$(MSBuildProjectName)\</BaseOutputPath>
    <BaseIntermediateOutputPath>$(RepoRoot)obj\$(Configuration)\$(MSBuildProjectName)\</BaseIntermediateOutputPath>
  </PropertyGroup>

</Project>

ConsoleApp1.csproj:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net46</TargetFramework>
    <OutputType>Exe</OutputType>
    <OutDir>$(RepoRoot)bin\$(Configuration)</OutDir>
  </PropertyGroup>

</Project>

@KirillOsenkov
Copy link
Member

Never mind, $(Configuration) isn't even set by the time Directory.Build.props is running, so my Directory.Build.props can't rely on $(Configuration) at all.

@dsplaisted
Copy link
Member Author

Updated straw man:

  • OutputPath: <RootOutputPath>\bin\<MSBuildProjectName>\[Platform]\<Configuration>\[TargetFramework]\[RuntimeIdentifier]
  • IntermediateOutputPath: <RootOutputPath>\obj\<MSBuildProjectName>\[Platform]\<Configuration>\[TargetFramework]\[RuntimeIdentifier]
  • PackageOutputPath: <RootOutputPath>\packages\<Configuration>

This swaps the position of Platform and Configuration, to match what the current logic in MSBuild and the .NET SDK use.

@dsplaisted
Copy link
Member Author

I think the following are the files where changes need to be made to support this:

  • Microsoft.Common.props - Use RootOutputPath to calculate BaseIntermediateOutputPath and MSBuildProjectExtensionsPath
  • Microsoft.Common.CurrentVersion.targets - Update OutputPath and IntermediateOutputPath logic to use RootOutputPath if it's set
  • Microsoft.NET.DefaultOutputPaths.targets (in dotnet/sdk repo) - Use RootOutputPath in output and intermediate output path calculation

We might also be able to unify some of the logic between Microsoft.Common.CurrentVersion.targets and Microsoft.NET.DefaultOutputPaths.targets, but that may be more complex / risky than would be worth it.

RootOutputPath should be set before the MSBuild common .props are evaluated, typically by including it in a Directory.Build.props file. If it's not set before then (for example if it's set in the body of a project file), then all paths except the MSBuildProjectExtensionsPath should be derived from it, and a warning should be generated. We already do this for setting BaseIntermediateOutputPath, for example:

warning MSB3539: The value of the property "BaseIntermediateOutputPath" was modified after it was used by MSBuild which can lead to unexpected build results. Tools such as NuGet will write outputs to the path specified by the "MSBuildProjectExtensionsPath" instead. To set this property, you must do so before Microsoft.Common.props is imported, for example by using Directory.Build.props. For more information, please visit https://go.microsoft.com/fwlink/?linkid=869650

We should either update this warning to also cover RootOutputPath, or introduce a new, similar warning.

@livarcocc livarcocc added this to the MSBuild 16.6 milestone Jan 2, 2020
@KirillOsenkov
Copy link
Member

This is what I came up with a while back:
https://gist.github.com/KirillOsenkov/2330e9b358f8801d176e84dffd2e98ee

@Nirmal4G
Copy link
Contributor

Nirmal4G commented Jan 8, 2020

@dsplaisted I'm not a fan of deep folder hierarchy.

Can't we take all the variables that identifies as a build parameter and put them into single property and use that property in the path. That way we can reuse that property later on and also have an identifier that effectively identifies a build config.

What I did before:

In the Microsoft.NET.DefaultOutputPaths.props/Microsoft.Common.props

<!--
	We need to initialize `BuildFolder` separately and before the `MSBuild.OutputPaths.targets` import,
	since `MSBuildProjectExtensionsPath` uses it to import custom props from Package Managers and Tools.
-->
<PropertyGroup Label="Build">
	<BuildFolder Condition="'$(BuildFolder)' == ''">build</BuildFolder>
	<BuildPath Condition="'$(BuildPath)' == ''">$(BuildFolder)\</BuildPath>
	<BuildPath Condition="!HasTrailingSlash('$(BuildPath)')">$(BuildPath)\</BuildPath>
	<BuildPath Condition="$([System.IO.Path]::IsPathRooted('$(BuildPath)')) AND !$(BuildPath.StartsWith('$(MSBuildProjectDirectory)'))">$(BuildPath)$(BuildFolder)\$(MSBuildProjectName)\</BuildPath>
	<_InitialBuildPath>$(BuildPath)</_InitialBuildPath>
</PropertyGroup>

In the Microsoft.NET.DefaultOutputPaths.targets/Microsoft.Common.targets

<PropertyGroup Label="Build">
	<IntermediateOutputFolder Condition="'$(IntermediateOutputFolder)' == ''">obj</IntermediateOutputFolder>
	<BaseIntermediateOutputPath>$(BuildPath)$(IntermediateOutputFolder)\</BaseIntermediateOutputPath>
	<IntermediateOutputPath>$(BaseIntermediateOutputPath)</IntermediateOutputPath>

	<OutputFolder Condition="'$(OutputFolder)' == ''">bin</OutputFolder>
	<BaseOutputPath>$(BuildPath)$(OutputFolder)\</BaseOutputPath>
	<OutputPath>$(BaseOutputPath)</OutputPath>
</PropertyGroup>

And wherever we want to append a property that differentiates a build, we do this there... make sure that property won't change afterwards.

For e.g.:

<!-- Each property is appended where it's set after and won't change afterwards -->
<PropertyGroup Condition="'$(Configuration)' != ''">
	<BuildContext Condition="!$(BuildContext.EndsWith('-'))">$(BuildContext)-</BuildContext>
	<BuildContext>$(BuildContext)$(Configuration)</BuildContext>
</PropertyGroup>

<PropertyGroup Condition="'$(Platform)' != '' AND '$(Platform)' != 'AnyCPU'">
	<BuildContext Condition="!$(BuildContext.EndsWith('-'))">$(BuildContext)-</BuildContext>
	<BuildContext>$(BuildContext)$(Platform)</BuildContext>
</PropertyGroup>

Then, we append the build identifier to the path at the very last in property evaluation or may be, if possible, first in one of the preparation targets...

<!-- Place these at last in evaluation or first in targets -->
<PropertyGroup>
	<BuildContext Condition="'$(BuildContext.Trim('-'))' != ''">$(BuildContext.Trim('-'))</BuildContext>
</PropertyGroup>

<PropertyGroup Condition="'$(BuildContext)' != ''">
	<IntermediateOutputPath>$(IntermediateOutputPath)$(BuildContext)\</IntermediateOutputPath>
	<OutputPath>$(OutputPath)$(BuildContext)\</OutputPath>
</PropertyGroup>

That's how I'm currently using it.

Note

I created it when the first .NET Sdk was released and have never changed the logic, even though v1->v2->v3 had some changes that broke many people's build.

@Nirmal4G
Copy link
Contributor

Nirmal4G commented Jan 8, 2020

Here's my Gist that I use in both Sdk and Legacy projects:

MSBuild Output Configurations

@Nirmal4G
Copy link
Contributor

Nirmal4G commented Apr 5, 2020

Any status on this?

@dsplaisted
Copy link
Member Author

We'd like to do this in .NET 5, but we're not sure if we'll be able to or not yet.

@jtbrower
Copy link

jtbrower commented Jun 6, 2020

I most definitely vote yes on this feature. As I mentioned in #3483, some of these work-arounds mentioned might run into issues with WPF projects.

@Nirmal4G
Copy link
Contributor

Nirmal4G commented Jul 29, 2020

Here's my take on this via BuildDir property: Nirmal4G/MSBuild@a9563c0

SDK's side: Nirmal4G/dotnet-sdk@ab012dc

  • It does introduce build^ via BuildDir and publish^ via PublishDir in the project root.
  • Uses BuildDir for Path mismatch warning between props/targets.
  • It moves MSBuildProjectExtensionsPath to BuildDir.

Thus, freeing up BaseIntermediateOutputPath from Common props. I believe this will serve up nicely in years to come.

^Note: we can prepend ~ in order to differentiate it from source folders. We could also have BuildDirName and use the existing PublishDirName to make the folder names overridable!

@agocke
Copy link
Member

agocke commented Mar 28, 2021

@dsplaisted Can I give a gentle nudge on this for .NET 6? 😄 I run into this on every new project.

@Forgind
Copy link
Member

Forgind commented Mar 28, 2021

@agocke There's already a PR out (#6105), though it isn't finished yet. We'd also like to fix this in the .NET 6 timeframe.

@Rast1234
Copy link

Rast1234 commented May 24, 2023

A most simple workaround i was able to hack after reading all related info here, because other published examples messed up my builds for whatever reason. I'm using it for dotnet publish exclusively and only with absolute path supplied from CLI.

Directory.Build.props

<Project>
  <PropertyGroup>
    <PublishDir>$(SmartOutputPath)\$(MSBuildProjectName)</PublishDir>
  </PropertyGroup>
</Project>

Usage

cp Directory.Build.props ./ # from whatever place you store ci-cd related files
dotnet publish src/Apps.sln -p:SmartOutputPath=$PWD/publish # abs paths only!

Results:

  • publish/Apps.Project1/...
  • publish/Apps.Project2/...

@japj
Copy link

japj commented May 24, 2023

I was wondering, isn’t this now covered by the dotnet 8 sdk?
https://devblogs.microsoft.com/dotnet/announcing-dotnet-8-preview-3/#simplified-output-path

@dsplaisted
Copy link
Member Author

dsplaisted commented May 25, 2023

I was wondering, isn’t this now covered by the dotnet 8 sdk? https://devblogs.microsoft.com/dotnet/announcing-dotnet-8-preview-3/#simplified-output-path

Yes, ArtifactsPath in the .NET 8 SDK should address this.

@Rast1234
Copy link

I see that in .net 8 artifacts are flattened by project, but not by structure if projects were placed in different subfolders. What happens if i have a solution with src/App/App.csproj and src/tests/App.csproj? in older SDKs with the publish -o option it was not trivial to detect that artifacts were colliding (imo it should be an error).

@dsplaisted
Copy link
Member Author

I see that in .net 8 artifacts are flattened by project, but not by structure if projects were placed in different subfolders. What happens if i have a solution with src/App/App.csproj and src/tests/App.csproj? in older SDKs with the publish -o option it was not trivial to detect that artifacts were colliding (imo it should be an error).

@Rast1234 This comment shows how you can preserve the project structure in the output path.

It would be ideal if we could generate an error if there is a conflict, but since the projects are built separately I can't think of a good way that we would detect this.

@rainersigwald
Copy link
Member

closing in favor of ArtifactsPath.

@rainersigwald rainersigwald closed this as not planned Won't fix, can't repro, duplicate, stale Jul 18, 2023
@Nirmal4G
Copy link
Contributor

The ArtifactsPath only works in projects that use the .NET SDK. What about the other projects in a solution? This issue is about having a common path structure for all types of projects that MSBuild / VS IDE supports.

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

Successfully merging a pull request may close this issue.