Ampelofilosofies

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

Out-of-band DotNet core

12 Jun 2019

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").

So nowadays I 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</BaseOutputPath>
      <BaseIntermediateOutputPath>$(BaseOutputPath)</BaseIntermediateOutputPath>
  </PropertyGroup>
</Project>

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

And then there is a BuildProperties.proj 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>

This is then added to each project file with an import statement:

<Import Project="..\BuildProperties.proj" />

You also 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.

Out of band builds ftw!

out/ should be the only entry in .gitignore.

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

blog comments powered by Disqus