How to build / publish self contained .NET Core binaries for Windows, Linux and OSX

The introduction of the .NET Core stack (and .NET Standard) has certainly been accompanied with some growing pains. The most prominent (wildly discussed) example would be the change in project file format from .csproj (XML based) to .xproj (JSON based). And then back to .csproj. The associated tooling has been somewhat rough around the edges (to a certain extent that still holds true today).

As a part of Microsofts remarkably more modern software development approach, a lot of the work is now performed out in the open on GitHub (with real community involvement as well). Changes occur frequently, resulting in a lot of outdated information available online, undoubtedly causing some confusion for newcomers and seasoned veterans alike. That being said, .NET Core still embodies some of the most fundamental (and ambitious) changes in the history of .NET.

Note: the following framework and tool versions were used for this blog post:

  • .NET Core Framework: 1.1.2
  • .NET Core Framework Host: 1.1.0
  • .NET Core Tooling (SDK): 1.0.4

In order to retrieve the shared framework host version, simply type dotnet at the command line:

C:\>dotnet

Microsoft .NET Core Shared Framework Host

  Version  : 1.1.0
  Build    : 928f77c4bc3f49d892459992fb6e1d5542cb5e86

Usage: dotnet [common-options] [[options] path-to-application]
...

Muliple framework versions can be installed side-by-side (in Windows, the default install location is C:\Program Files\dotnet\shared\Microsoft.NETCore.App).

The .NET Core Tooling version (SDK) can be retrieved by issuing dotnet --version:

C:\>dotnet --version
1.0.4

Project File Structure, anno 2017

With the above versions of the tooling, the new (and lean) .csproj structure can be used.

It can be quite short and concise (the sample is taken from my AVRDisassembler project):

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp1.1</TargetFramework>
    <OutputType>Exe</OutputType>
    <Title>AVRDisassembler</Title>
    <Version>0.2.0-beta</Version>
    <Description>A .NET Core Atmel AVR / Arduino disassembler.</Description>
    <Authors>Christophe Diericx</Authors>
    <CopyRight>Christophe Diericx</CopyRight>
    <PackageTags>arduino atmel avr disassembler hex</PackageTags>
    <PackageProjectUrl>https://github.com/christophediericx/AVRDisassembler</PackageProjectUrl>
    <PackageIconUrl>https://avatars3.githubusercontent.com/u/19640854?s=140</PackageIconUrl>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="CommandLineParser" Version="2.1.1-beta" />
    <PackageReference Include="IntelHexFormatReader" Version="2.2.2" />
    <PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
  </ItemGroup>
</Project>

The following section indicates this project is a command line application, targetting the .NET Core Framework, version 1.1 (the absence of the OutputType tag would result in a class library being built instead):

<TargetFramework>netcoreapp1.1</TargetFramework>
<OutputType>Exe</OutputType>

These lines are really just adding some metadata:

<Title>AVRDisassembler</Title>
<Version>0.2.0-beta</Version>
<Description>A .NET Core Atmel AVR / Arduino disassembler.</Description>
<Authors>Christophe Diericx</Authors>
<CopyRight>Christophe Diericx</CopyRight>
<PackageTags>arduino atmel avr disassembler hex</PackageTags>
<PackageProjectUrl>https://github.com/christophediericx/AVRDisassembler</PackageProjectUrl>
<PackageIconUrl>https://avatars3.githubusercontent.com/u/19640854?s=140</PackageIconUrl>

Although strictly optional, defining those in the project file allows for the following incredibely convenient trick: dotnet pack -c release -o [myoutputdir] will automatically generate us a (well specified) nuget package:

C:\Source\AVRDisassembler\Source\AVRDisassembler\AVRDisassembler>dotnet pack -c release -o C:\Packages
Microsoft (R) Build Engine version 15.1.1012.6693
Copyright (C) Microsoft Corporation. All rights reserved.

  AVRDisassembler -> C:\Source\AVRDisassembler\Source\AVRDisassembler\AVRDisassembler\bin\release\netcoreapp1.1\AVRDisassembler.dll
  Successfully created package 'C:\Packages\AVRDisassembler.0.2.0-beta.nupkg'.

That leaves us with just our (nuget) dependency definitions in the file:

<ItemGroup>
  <PackageReference Include="CommandLineParser" Version="2.1.1-beta" />
  <PackageReference Include="IntelHexFormatReader" Version="2.2.2" />
  <PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
</ItemGroup>

Types of .NET Core deployments

.NET Core supports two distinct ways of deploying applications.

  • Framework-Dependent Deployments (FDD) - the default deployment model
    • Deploys your application and any external dependencies
    • Relies on a (compatible) .NET Core Runtime being present on the target system

Both are performed through issuing a dotnet publish command.

The advantages and disadvantages of each approach are explained quite well in the relevant sections of the documentation linked above. For the purpose of this blog post, we will only focus on the “self-contained” deployment model, which results in very “portable” binaries (in the sense that no assumptions about the presence of a .NET Core runtime must be made). A “Hello World” SCD will weigh in at around 50Mb.

Target Operating Systems for Self-Contained Deployments

In order to configure a Self-Contained Deployment, we will add one or more RID (Runtime IDentifier) tokens to our project file. The presence (or absence) of these tokens is what makes dotnet publish produce either a “Self-Contained” or “Framework-Dependent” build.

The catalog of Runtime Identifiers details our options. As is noted on the page itself, identifiers get added often (with documentation lagging behind). In case of doubt, the underlying runtime.json file might be a good source of valid identifiers. Another snippet of the documentation on GitHub describes how these identifiers are defined in a graph that can be interpreted for compatibility purposes.

For most purposes, using simple unambiguous (explicit) identifiers should do the trick though.

For our sample, we will assume that want to simultaneously target:

  • Windows 7 (x64) (upwards compatible with Win 8, 8.1 and 10)
  • Ubuntu 16.10 (x64)
  • OS X Sierra (10.12) (x64)

In order to do this, we add the following to our project file:

<RuntimeIdentifiers>win7-x64;ubuntu.16.10-x64;osx.10.12-x64</RuntimeIdentifiers>

Our complete project file now looks like this:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp1.1</TargetFramework>
    <OutputType>Exe</OutputType>
    <Title>AVRDisassembler</Title>
    <Version>0.2.0-beta</Version>
    <Description>A .NET Core Atmel AVR / Arduino disassembler.</Description>
    <Authors>Christophe Diericx</Authors>
    <CopyRight>Christophe Diericx</CopyRight>
    <PackageTags>arduino atmel avr disassembler hex</PackageTags>
    <PackageProjectUrl>https://github.com/christophediericx/AVRDisassembler</PackageProjectUrl>
    <PackageIconUrl>https://avatars3.githubusercontent.com/u/19640854?s=140</PackageIconUrl>
    <RuntimeIdentifiers>win7-x64;ubuntu.16.10-x64;osx.10.12-x64</RuntimeIdentifiers>    
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="CommandLineParser" Version="2.1.1-beta" />
    <PackageReference Include="IntelHexFormatReader" Version="2.2.2" />
    <PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
  </ItemGroup>
</Project>

Building Deployment Packages

The sequence of commands we need (to build all three) is:

dotnet clean
dotnet restore
dotnet build -c release
dotnet publish -c release -r win7-x64 -o bin/dist/win-x64
dotnet publish -c release -r ubuntu.16.10-x64 -o bin/dist/ubuntu.16.10-x64
dotnet publish -c release -r osx.10.12-x64 -o bin/dist/osx.10.12-x64
  • Both dotnet clean (and the explicit dotnet build) are optional in this flow
  • dotnet restore will download the corresponding required packages
  • dotnet publish will do the heavy lifting as far as building the deployment packages is concerned

After running these commands, one will find self-contained versions (for three distinct Operating Systems) in the corresponding bin/dist folders.