REAL Single File Publish/Build for `dotnet`
Quick guide on how to publish a dotnet project to a single file executable with none of those annoyingly lingering `.pdb` or `.dll` files.
Update 2021-11-09: Updated examples to use net6.0 and the new
EnableCompressionInSingleFile
flag: https://devblogs.microsoft.com/dotnet/announcing-net-6/#compression
Property Group / MSBuild Configurations
You can set MSBuild
properties in the PropertyGroup
section of the
<project-name>.(c|f)sproj
file or via the command-line by passing them in via
-p:<PropertyName>=<PropertyValue>
.
Example:
dotnet publish \
-p:TargetFramework=net6.0 \
-p:RuntimeIdentifier=osx-x64 \
...
Is the same as having a <project-name>.(cs|fs)proj
file with:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RuntimeIdentifier>osx-x64</RuntimeIdentifier>
</PropertyGroup>
</Project>
Required:
TargetFramework
:string
- Target version of dotnet; will be auto filled into your
.(fs|cs)proj
file if created viadotnet new
- eg:
net6.0
- Target version of dotnet; will be auto filled into your
RuntimeIdentifier
:string
- Host OS/arch identifier.
- eg:
osx-x64
PublishSingleFile
:boolean
- Publish to a single binary file instead of a folder with a bunch of
.dll
. - Requires
SelfContained
. - https://docs.microsoft.com/en-us/dotnet/core/deploying/single-file#publish-a-single-file-app---sample-project-file
- Publish to a single binary file instead of a folder with a bunch of
SelfContained
:boolean
- Bundle dotnet runtime with the published build (instead of depending on the host to have it).
IncludeNativeLibrariesForSelfExtract
:boolean
- Include native libraries in the single file bundle that normally get written
to the binary output directory even with
PublishSingleFile
enabled. - Example native files that will show up beside your binary (from macOS):
libSystem.IO.Compression.Native.dylib libSystem.Native.dylib libSystem.Net.Security.Native.dylib libSystem.Security.Cryptography.Native.Apple.dylib libSystem.Security.Cryptography.Native.OpenSsl.dylib libclrjit.dylib libcoreclr.dylib
- Include native libraries in the single file bundle that normally get written
to the binary output directory even with
DebugType
:string
- Bundle the
.pdb
into the outputted executable - https://docs.microsoft.com/en-us/dotnet/core/deploying/single-file#include-pdb-files-inside-the-bundle
- Bundle the
Optional:
PublishTrimmed
:boolean
- Tree-shake the binary.
- Makes binary smaller.
PublishReadyToRun
:boolean
- AOT compile
- Makes start-time faster.
- Makes binary larger.
EnableCompressionInSingleFile
:boolean
(added in .NET 6)- Compress the binary.
- Makes binary smaller.
ServerGarbageCollection
:boolean
- https://docs.microsoft.com/en-us/dotnet/core/run-time-config/garbage-collector
- Set to
true
if you know you have beefy hardware and know that your app is going to be abusing the GC. - Can drastically improve performance if you have a lot of GC pressure. For example if doing work with tree data structures.
The All-In-One Script
dotnet publish \
-p:TargetFramework=net6.0 \
-p:RuntimeIdentifier=osx-x64 \
-p:SelfContained=true \
-p:PublishSingleFile=true \
-p:IncludeNativeLibrariesForSelfExtract=true \
-p:PublishTrimmed=true \
-p:PublishReadyToRun=true \
-p:EnableCompressionInSingleFile=true \
-p:DebugType=embedded \
-p:ServerGarbageCollection=true \
--output dist
Some options are passable as top level cli flags instead of property injection:
dotnet publish \
--framework net6.0 \
--runtime osx-x64 \
--self-contained true \
-p:PublishSingleFile=true \
-p:IncludeNativeLibrariesForSelfExtract=true \
-p:PublishTrimmed=true \
-p:PublishReadyToRun=true \
-p:EnableCompressionInSingleFile=true \
-p:DebugType=embedded \
-p:ServerGarbageCollection=true \
--output dist
What I Use
Having all those options in terminal is annoying, I usually put everything in my
project file except the RuntimeTarget
:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<PublishTrimmed>true</PublishTrimmed>
<PublishReadyToRun>true</PublishReadyToRun>
<DebugType>embedded</DebugType>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
</Project>
Then I can build for all platforms with:
dotnet publish --runtime osx-x64 --output dist/darwin-amd64
dotnet publish --runtime win10-x64 --output dist/win-amd64
dotnet publish --runtime linux-x64 --output dist/linux-amd64
# Equivalent to:
# dotnet publish -p:RuntimeIdentifier osx-x64 --output dist/darwin-amd64
# dotnet publish -p:RuntimeIdentifier win10-x64 --output dist/win-amd64
# dotnet publish -p:RuntimeIdentifier linux-x64 --output dist/linux-amd64
Almighty Makefile
This is the all in one Makefile which I use for most of my projects. Their are
some nobs which I don't turn such as ServerGarbageCollection
and
PublishReadyToRun
because they just aren't needed in my day-to-day builds or
because I prefer smaller executables than shaving ms from start times.
.PHONY: all build clean publish compress
# ==============================================================================
# Variables
# ==============================================================================
override BUILD_OUTPUT_LIN_DIR = $(DIST_DIR)/$(DOTNET_RUNTIME_LIN)
override BUILD_OUTPUT_LIN_EXE = $(BUILD_OUTPUT_LIN_DIR)/$(EXECUTABLE_NAME)
override BUILD_OUTPUT_MAC_DIR = $(DIST_DIR)/$(DOTNET_RUNTIME_MAC)
override BUILD_OUTPUT_MAC_EXE = $(BUILD_OUTPUT_MAC_DIR)/$(EXECUTABLE_NAME)
override BUILD_OUTPUT_WIN_DIR = $(DIST_DIR)/$(DOTNET_RUNTIME_WIN)
override BUILD_OUTPUT_WIN_EXE = $(BUILD_OUTPUT_WIN_DIR)/$(EXECUTABLE_NAME).exe
override COMPRESSED_LIN_PATH = $(DIST_DIR)/$(EXECUTABLE_NAME)-$(DOTNET_RUNTIME_LIN).tar.gz
override COMPRESSED_MAC_PATH = $(DIST_DIR)/$(EXECUTABLE_NAME)-$(DOTNET_RUNTIME_MAC).tar.gz
override COMPRESSED_WIN_PATH = $(DIST_DIR)/$(EXECUTABLE_NAME)-$(DOTNET_RUNTIME_WIN).zip
override DIST_DIR = dist
override DOTNET_RUNTIME_LIN = linux-x64
override DOTNET_RUNTIME_MAC = osx-x64
override DOTNET_RUNTIME_WIN = win10-x64
override DOTNET_VERSION = net6.0
override EXECUTABLE_NAME = $(error "Please set EXECUTABLE_NAME to the name of your executable")
override TESTS_PATH = $(error "Please set TESTS_PATH to relative path to test project")
# ==============================================================================
# Targets
# ==============================================================================
all: clean test publish
build: $(BUILD_OUTPUT_MAC_EXE) $(BUILD_OUTPUT_LIN_EXE) $(BUILD_OUTPUT_WIN_EXE)
compress: $(COMPRESSED_MAC_PATH) $(COMPRESSED_LIN_PATH) $(COMPRESSED_WIN_PATH)
publish: build compress
clean:
dotnet clean
-rm -rf $(DIST_DIR)
test:
dotnet run --project $(TESTS_PATH)
test_watch:
dotnet watch --project $(TESTS_PATH)
# Build
$(BUILD_OUTPUT_MAC_EXE):
dotnet publish \
-p:TargetFramework=$(DOTNET_VERSION) \
-p:RuntimeIdentifier=$(DOTNET_RUNTIME_MAC) \
-p:SelfContained=true \
-p:PublishSingleFile=true \
-p:IncludeNativeLibrariesForSelfExtract=true \
-p:PublishTrimmed=true \
-p:DebugType=embedded \
-p:EnableCompressionInSingleFile=true \
--output $(BUILD_OUTPUT_MAC_DIR)
$(BUILD_OUTPUT_LIN_EXE):
dotnet publish \
-p:TargetFramework=$(DOTNET_VERSION) \
-p:RuntimeIdentifier=$(DOTNET_RUNTIME_LIN) \
-p:SelfContained=true \
-p:PublishSingleFile=true \
-p:IncludeNativeLibrariesForSelfExtract=true \
-p:PublishTrimmed=true \
-p:DebugType=embedded \
-p:EnableCompressionInSingleFile=true \
--output $(BUILD_OUTPUT_LIN_DIR)
$(BUILD_OUTPUT_WIN_EXE):
dotnet publish \
-p:TargetFramework=$(DOTNET_VERSION) \
-p:RuntimeIdentifier=$(DOTNET_RUNTIME_WIN) \
-p:SelfContained=true \
-p:PublishSingleFile=true \
-p:IncludeNativeLibrariesForSelfExtract=true \
-p:PublishTrimmed=true \
-p:DebugType=embedded \
-p:EnableCompressionInSingleFile=true \
--output $(BUILD_OUTPUT_WIN_DIR)
# Compress
$(COMPRESSED_MAC_PATH): $(BUILD_OUTPUT_MAC_EXE)
tar -zcvf $(COMPRESSED_MAC_PATH) \
--directory=$(BUILD_OUTPUT_MAC_DIR) $(EXECUTABLE_NAME)
$(COMPRESSED_LIN_PATH): $(BUILD_OUTPUT_LIN_EXE)
tar -zcvf $(COMPRESSED_LIN_PATH) \
--directory=$(BUILD_OUTPUT_LIN_DIR) $(EXECUTABLE_NAME)
$(COMPRESSED_WIN_PATH): $(BUILD_OUTPUT_WIN_EXE)
cd $(BUILD_OUTPUT_WIN_DIR) \
&& zip ../$(EXECUTABLE_NAME)-$(DOTNET_RUNTIME_WIN).zip $(EXECUTABLE_NAME).exe