Publishing a self-contained single-file .NET 5 executable
Assuming you want to target Windows:
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true
And if you want to reduce the size (taking advantage of trimming):
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishTrimmed=True -p:TrimMode=Link
If you'd like some background and an explanation of those options then read on.
Background and Motivation
I build a small benchmarking tool (SQLDriver), and after updating to .NET 5 recently I realized that anyone using it may need to install an updated runtime. I'd previously made an effort to keep the executable as easy to download and use as possible (using ILMerge to bundle dependent assemblies into the executable), so wondered how easy things were with .NET 5?
Self-Contained
A self-contained .NET application is one that doesn't rely on there being any shared components on the target machine (such as the .NET runtime). This feature has actually been around for several years, and the only downside is that it can end up publishing a lot of additional files along with the app (which also means a simple console app can be 60MB+).
Getting this to work requires a single flag:
dotnet publish -c Release -r win-x64 --self-contained true
Read more on the Microsoft docs site: Self-contained publishing.
Single-File
Rather than dozens (or hundreds) of files, this option publishes only a handful of files, with the rest being unzipped into memory when the app is launched:
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true
The reason there isn't a single file (which is what you might expect!) is that only managed DLLs are bundled into the executable, and the native binaries (part of the .NET runtime) are left as separate files. You can have the native binaries bundled by specifying the following:
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true
In .NET Core 3.x the
PublishSingleFile
option also bundled native binaries. TheIncludeNativeLibrariesForSelfExtract
option is new wih .NET 5, where the default is to not bundle the native binaries.
Read more on the Microsoft docs site: Single-file deployment and executable.
Trimming
The final step for me was to enable trimming. Available only for self-contained apps, this feature allows bundling of only the assemblies that are used (rather than the entire .NET runtime):
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishTrimmed=True
We can go one step further and remove unused code from assemblies (rather than whole assemblies only):
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishTrimmed=True -p:TrimMode=link
The downside to trimming (including the more aggressive link
option) is that there are certain scenarios where the build-time analysis may incorrectly identify code as unused, which will result in a runtime error. The docs call out components that can cause trimming problems - and in my case the application was very simple and also very easy to test for correctness.
Read more on the Microsoft docs site: Trim self-contained deployments and executables.
Conclusion
As a result of the self-contained, single-file, and trimming options I was able to get SQLDriver down to a single ~18MB executable that doesn't require anything to be installed on the target machine.