the personal playground of evan louie; developer, designer, photographer, and breaker of the web.
   (  )   /\   _                 (     
    \ |  (  \ ( \.(               )                      _____
  \  \ \  `  `   ) \             (  ___                 / _   \
 (_`    \+   . x  ( .\            \/   \____-----------/ (o)   \_
- .-               \+  ;          (  O                           \____
                          )        \_____________  `              \  /
(__                +- .( -'.- <. - _  VVVVVVV VV V\                 \/
(_____            ._._: <_ - <- _  (--  _AAAAAAA__A_/                  |
  .    /./.+-  . .- /  +--  - .     \______________//_              \_______
  (__ ' /x  / x _/ (                                  \___'          \     /
 , x / ( '  . / .  /                                      |           \   /
    /  /  _/ /    +                                      /              \/
   '  (__/                                             /                  \

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:

Optional:

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