Ampelofilosofies

homeaboutrss
The mind is like a parachute. If it doesn't open, you're meat.

Out-of-band DotNet core

12 Jun 2019

This entry was edited again on 2019-09-04 and updated so that no more project file entries are necessary. I am a proponent of “out-of-band” builds. This simply means that any and all files generated by the build process should not under any circumstances be found next to sources.

The simplest check for this is doing a git status after building and seeing no untracked files, provided your .gitignore is (almost) empty.

Correspondingly I find long .gitignore files simply annoying and an indication of sloppy development practices.

This is a long saga, there are rant entries in this blog from way back in 2016.

Visual Studio was my greatest adversary in the quest for clean workspaces…until DotNet core came out.

Previously in the saga we figured out how to get the intermediate compilation results out of the way by setting <IntermediateOutputPath>.

But dotnet does not respect this property. It uses instead <BaseIntermediateOutputPath>. Things like nuget specs and package assets (properties and tasks for nuget packages etc.) will land in this directory.

OK, you say, I’ll set that in the project file and be done. And then you spent a couple of hours trying to figure out why the setting is not respected.

It all has to do with the way msbuild defines properties and figuring out which property to use and when it is defined.

See, in the drive to have everything “just work” when you create a dotnet core project in Visual Studio the project element will have a magic Sdk attribute:

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

This, very slyly, instructs msbuild to import a whole bunch of property and task files. No need for you to know all the dirty details, innit?

And if you try to override it late, you will get a nice warning

C:\Program Files\dotnet\sdk\2.2.300\Microsoft.Common.CurrentVersion.targets(813,5): 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.

Directory.Build.props is one of those “magic” files. It is loaded very early and you can set these properties there. It will get picked up if it is there and msbuild will search far and wide for it (it will go up the directory tree and then look in a few places more).

Not that simple

Timing is everything it seems. There are things you cannot put in Directory.Build.props. For example, the AssemblyName property is not set yet when the file is processed, so we cannot really set OutputPath and IntermediateOutputPath to safely differentiate compilation artifacts.

The one thing we learned the hard way: Always add $(Configuration)somewhere in the path otherwise you will get very wierd results (as in “honey, I linked the wrong binaries”).

To be able to use property values that are set later, use Directory.Build.targets which is read after msbuild has loaded its task files.

So we split the difference

Directory.Build.props is very basic, as follows:

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <BaseOutputPath>$(MSBuildThisFileDirectory)\..\..\out\build\$(MSBuildProjectName)\</BaseOutputPath>
    <BaseIntermediateOutputPath>$(BaseOutputPath)\obj</BaseIntermediateOutputPath>
  </PropertyGroup>
</Project>

Notice the use of MSBuildThisFileDirectory in the base path, that allows us to define the path relative to the property file.

Directory.Build.targets is where we set project and configuration specific paths:

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <OutputPath>$(BaseOutputPath)\$(AssemblyName)\bin\$(Configuration)</OutputPath>
    <IntermediateOutputPath>$(BaseIntermediateOutputPath)$(AssemblyName)\obj\$(Configuration)</IntermediateOutputPath>
  </PropertyGroup>
</Project>

And we’re done. There is though one catch, especially if you have a lot of .NET framework projects that do not make use of the SDK simplifications.

You need to remove any OutputPath definitions in conditional entries (usually the Condition=”’$(Configuration)|$(Platform)’==’Debug|AnyCPU’” and Condition=”’$(Configuration)|$(Platform)’==’Release|AnyCPU’” entries. Visual Studio does not expose IntermediateOutputPath in the GUI so you will not see an entry unless you add it.

You also need to be aware that when editing properties in Visual Studio the values are changed in the project files, which will overrided any entries in the Directory.Build files.

Out of band builds ftw!

out/ should be the only entry in .gitignore.

This setup will force msbuild, dotnet and VS to at least compile everything in out/build and stop littering the workspace.

blog comments powered by Disqus