Bending .NET - Corrected Common Flat Build Output
or how gathering and destroying all human rebels in Zion failed due to renegade program.
UPDATE 2022-01-24: The approach defined here has been improved in Bending .NET - Improved Common Flat Build Output
In this post, part of the Bending .NET series, I try to correct the ship wreck that was Bending .NET - Common Flat Build Output, which had a serious flaw causing Go To Definition (F12) not to work, as now covered in that blog post.
Source: pixabay
In the previous blog post I thought I had finally found a way to define common
flat build output without having to change anything in csproj
-files.
Unfortunately that had issues and I have found no way to accomplish this without
changing csproj
-files, so in this blog post I present the for now best
solution I could find; including a common props
-file at the end of each
csproj
-file.
At the same time I try to simplify the properties changed to only focus on flattening the final build and publish output. Prerequisites are the same as in the previous blog post, so without further ado I will present the new approach.
A new file OutputBuildProject.props
is added to the src
level common files.
1
2
3
4
5
.\src\Directory.Build.props
.\src\Directory.Build.targets
.\src\OutputBuildProps.props
.\src\OutputBuildProject.props
.\src\OutputBuildTargets.props
Again, OutputBuildProps.props
is imported by Directory.Build.props
and OutputBuildTargets.props
is imported by Directory.Build.targets
,
but OutputBuildProject.props
then has to be imported explicitly in
each csproj
-file. This is very annoying and cumbersome if you have a lot
of projects in a solution, but I’ve found no other way.
Directory.Build.props
is shown below and is pretty straight-forward.
1
2
3
4
5
6
7
8
9
<Project>
<PropertyGroup>
<!-- Other common properties omitted for brevity -->
<Deterministic>true</Deterministic>
<LangVersion>10.0</LangVersion>
</PropertyGroup>
<Import Project="$(MSBuildThisFileDirectory)\OutputBuildProps.props" />
</Project>
OutputBuildProps.props
is shown below and both defines custom properties for
easy reuse and overrides the most important top-level properties like
BaseIntermediateOutputPath
and BaseOutputPath
. Note that this includes a new
property BasePublishDir
, since we now separate the published output from build
output in a new top-level directory publish
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<Project>
<PropertyGroup Label="OutputBuildProps">
<Platform Condition="$(Platform) == ''">AnyCPU</Platform>
<Configuration Condition="$(Configuration) == ''">Debug</Configuration>
<!-- Custom properties -->
<BuildDir>$(MSBuildThisFileDirectory)..\build\</BuildDir>
<ProjectBuildDirectoryName>$(MSBuildProjectName)_$(Platform)_$(Configuration)</ProjectBuildDirectoryName>
<OutputPathWithoutEndSlash>$(BuildDir)$(ProjectBuildDirectoryName)</OutputPathWithoutEndSlash>
<BaseOutDir>$(OutputPathWithoutEndSlash)</BaseOutDir>
<BasePublishDir>$(MSBuildThisFileDirectory)..\publish\</BasePublishDir>
<!-- MSBuild defined properties redefined -->
<BaseIntermediateOutputPath>$(BuildDir)obj\$(MSBuildProjectName)\</BaseIntermediateOutputPath>
<BaseOutputPath>$(BuildDir)bin\$(MSBuildProjectName)\</BaseOutputPath>
<PackageOutputPath>$(BaseOutDir)</PackageOutputPath>
</PropertyGroup>
</Project>
Directory.Build.targets
basically just forwards to OutputBuildTargets.props
,
the reason for this is to allow a specific git repository to still override
or define other properties as needed, but still be able to easily update the
common build output properties by pasting a file.
1
2
3
<Project>
<Import Project="$(MSBuildThisFileDirectory)\OutputBuildTargets.props" />
</Project>
OutputBuildTargets.props
is shown below. This no longer reassigns/overrides
any properties as that was the issue causing problems in Visual Studio. Again it
still includes a “hack” needed to cleanup WPF temporary output, as is discussed
in the linked issue.
1
2
3
4
5
6
7
8
9
10
11
12
<Project>
<!--
WPF projects output temporary assemblies in directories that are not deleted after use.
See https://github.com/dotnet/wpf/issues/2930
-->
<Target Name="RemoveWpfTemp" AfterTargets="Build">
<ItemGroup>
<WpfTempDirectories Include="$([System.IO.Directory]::GetDirectories("$(BuildDir)","$(MSBuildProjectName)*_wpftmp_*"))"/>
</ItemGroup>
<RemoveDir Directories="@(WpfTempDirectories)" />
</Target>
</Project>
OutputBuildProject.props
is the new file and this now sets the properties
defining the final build and publish output locations.
1
2
3
4
5
6
7
8
9
<Project>
<PropertyGroup>
<OutDir>$(BaseOutDir)_$(TargetFramework)\</OutDir>
<TargetDir>$(OutDir)</TargetDir>
<PublishDir>$(BasePublishDir)$(ProjectBuildDirectoryName)</PublishDir>
<PublishDir Condition="$(TargetFramework) != ''">$(PublishDir)_$(TargetFramework)</PublishDir>
<PublishDir Condition="$(RuntimeIdentifier) != ''">$(PublishDir)_$(RuntimeIdentifier)</PublishDir>
</PropertyGroup>
</Project>
Each project then needs to import this file for example at the end of the file as shown below.
1
2
3
4
5
6
7
8
9
10
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net5.0;net6.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<Import Project="$(MSBuildThisFileDirectory)..\OutputBuildProject.props" />
</Project>
Let’s build, publish and pack to be sure output is as expected.
1
2
3
4
5
6
7
8
dotnet build -c Debug
dotnet build -c Release
dotnet publish -c Debug .\src\CommonFlatBuild.AppWpf\CommonFlatBuild.AppWpf.csproj
dotnet publish -c Release .\src\CommonFlatBuild.AppWpf\CommonFlatBuild.AppWpf.csproj
dotnet publish -c Release -r win-x64 /p:PublishSingleFile=true --self-contained .\src\CommonFlatBuild.AppWpf\CommonFlatBuild.AppWpf.csproj
dotnet publish -c Release -r win-x86 /p:PublishSingleFile=true --no-self-contained .\src\CommonFlatBuild.AppWpf\CommonFlatBuild.AppWpf.csproj
dotnet publish -c Release -r linux-x64 /p:PublishSingleFile=true --self-contained .\src\CommonFlatBuild.AppConsole\CommonFlatBuild.AppConsole.csproj
dotnet pack -c Release
The end result in tree form (with details omitted) then is:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
├───build
│ ├───bin
│ │ └───CommonFlatBuild.Test
│ │ ├───Debug
│ │ │ └───net6.0
│ │ └───Release
│ │ └───net6.0
│ ├───CommonFlatBuild.AppConsole_AnyCPU_Debug_net6.0
│ ├───CommonFlatBuild.AppConsole_AnyCPU_Release
│ ├───CommonFlatBuild.AppConsole_AnyCPU_Release_net6.0
│ ├───CommonFlatBuild.AppWinForms_AnyCPU_Debug_net6.0-windows
│ ├───CommonFlatBuild.AppWinForms_AnyCPU_Release
│ ├───CommonFlatBuild.AppWinForms_AnyCPU_Release_net6.0-windows
│ ├───CommonFlatBuild.AppWpf_AnyCPU_Debug_net6.0-windows
│ ├───CommonFlatBuild.AppWpf_AnyCPU_Release
│ ├───CommonFlatBuild.AppWpf_AnyCPU_Release_net6.0-windows
│ ├───CommonFlatBuild.Test_AnyCPU_Debug_net6.0
│ ├───CommonFlatBuild.Test_AnyCPU_Release_net6.0
│ ├───CommonFlatBuild_AnyCPU_Debug_net5.0
│ ├───CommonFlatBuild_AnyCPU_Debug_net6.0
│ ├───CommonFlatBuild_AnyCPU_Release
│ ├───CommonFlatBuild_AnyCPU_Release_net5.0
│ ├───CommonFlatBuild_AnyCPU_Release_net6.0
│ └───obj
│ ├───CommonFlatBuild
│ │ ├───Debug
│ │ │ ├───net5.0
│ │ │ └───net6.0
│ │ └───Release
│ │ ├───net5.0
│ │ └───net6.0
│ ├───CommonFlatBuild.AppConsole
│ │ ├───Debug
│ │ │ └───net6.0
│ │ └───Release
│ │ └───net6.0
│ ├───CommonFlatBuild.AppWinForms
│ │ ├───Debug
│ │ │ └───net6.0-windows
│ │ └───Release
│ │ └───net6.0-windows
│ ├───CommonFlatBuild.AppWpf
│ │ ├───Debug
│ │ │ └───net6.0-windows
│ │ └───Release
│ │ └───net6.0-windows
│ └───CommonFlatBuild.Test
│ ├───Debug
│ │ └───net6.0
│ └───Release
│ └───net6.0
├───publish
│ ├───CommonFlatBuild.AppConsole_AnyCPU_Release_net6.0_linux-x64
│ ├───CommonFlatBuild.AppWpf_AnyCPU_Debug_net6.0-windows
│ ├───CommonFlatBuild.AppWpf_AnyCPU_Release_net6.0-windows
│ ├───CommonFlatBuild.AppWpf_AnyCPU_Release_net6.0-windows_win-x64
│ └───CommonFlatBuild.AppWpf_AnyCPU_Release_net6.0-windows_win-x86
└───src
├───CommonFlatBuild
├───CommonFlatBuild.AppConsole
├───CommonFlatBuild.AppWinForms
├───CommonFlatBuild.AppWpf
└───CommonFlatBuild.Test
Note that for intermediate build output the deep hierarchical output is not changed. The focus with the new approach is primarily on the final output files.
With this build and publish can be easily found and deleted like below:
1
rmdir build;rmdir publish
No more rebel humans and no more agent issues… I hope 🤞
In the hope that we might in the future be able to define common flat build
output without changing csproj
-files I have filed an issue on Github for
MSBuild.
PS: Example source code can be found in the GitHub repo for this blog nietras.github.io and as a zip-file CommonFlatBuild.zip.