Revision history (1 updates, last updated Sep 1, 2026)
A log of the changes made to this article. Where a pre-update version was archived, it stays readable at a permanent DOI link.
- Retranslated as a full translation of the Japanese original. The previous English version was an abridgement that carried only part of the source, so sections, tables, Mermaid diagrams, figure captions and FAQ entries were missing. All of them have been restored to match the Japanese original, and the technical claims are the same as in the Japanese version. Read the version before this update (DOI: 10.5281/zenodo.21614554)
- First published
Cite this article(DOI: 10.5281/zenodo.21614553)
This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.
Go Komura (2026). Single-File Distribution of Windows Apps - Single Binaries and the Limits of OS Dependencies. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21614553 https://comcomponent.com/en/blog/2026/03/19/003-windows-single-binary-and-os-dependencies/
- DOI (latest version)
- 10.5281/zenodo.21614553
- DOI (this version)
- 10.5281/zenodo.22217180
This article started from a casual conversation about how far you can go on Windows before something stops counting as a single binary. The post that set it off is below. You can skip it; everything after it reads on its own.
On Windows, “we’d like to ship this as a single file if possible” is a very ordinary request. For internal tools, equipment-integration tools, monitoring terminals, offline environments, and sites that want to avoid installers as much as possible, going single-binary is very appealing.
However, if you do not untangle this conversation at the start, it usually stops making sense partway through. That is because “we want a single binary” on Windows tends to mix four separate concerns.
- We want a single distribution artifact
- We want to avoid pre-installing the .NET or Visual C++ runtimes
- We want it to run by just dropping it in place, with no installer and no administrator rights
- We do not want to depend on differences between target Windows versions
These four are not the same thing. In practice, this framing keeps everyone aligned:
You can get quite far toward shipping a single EXE. But you cannot reduce the dependency on the target Windows to zero.
This article maps out that boundary for practical Windows app work.
flowchart TB
accTitle: A single EXE does not remove OS dependencies
accDescr: A diagram showing that the wish for a single binary mixes distribution concerns with dependency concerns, and that while consolidating the distribution into one EXE is largely achievable, the dependency on the target Windows cannot be reduced to zero.
a0["We want a single binary"] --> a1["Distribution: one artifact, drop-in deployment"]
a0 --> a2["Dependency: no runtime needed, no OS dependency"]
a1 --> a3["Consolidating into one EXE is largely achievable"]
a2 --> a4["OS dependency cannot be reduced to zero"]
Figure 1: The wish to ship one file mixes a feasible goal with an impossible one.
1. The Conclusion First
Summarizing the conclusion up front:
- For an ordinary desktop EXE, you can push single-binary packaging quite far
- However, being able to ship one EXE and not depending on the target Windows are different things
- For shell extensions, Windows services, drivers, WebView2, and parts of WinUI 3, the real issue is less the file count and more what you register with the OS and what you assume about it
- The most important thing in practice is to decide separately whether you want a single binary, no installer, or fewer OS dependencies
Put another way, the lines on Windows fall like this.
- Consolidating the distribution into one artifact: largely achievable
- Bundling additional runtimes: largely achievable
- Moving toward xcopy deployment: depends on the kind of app
- Eliminating dependencies on the target Windows itself: impossible
flowchart TB
accTitle: Where the lines fall on Windows
accDescr: A diagram showing that consolidating the distribution into one artifact and bundling additional runtimes are largely achievable, that xcopy deployment depends on the kind of app, and that eliminating dependencies on the target Windows is impossible.
b1["Consolidate into one artifact"] --> b2["Largely achievable"]
b3["Bundle additional runtimes"] --> b2
b4["Move toward xcopy deployment"] --> b5["Depends on the kind of app"]
b6["Eliminate OS-side dependencies"] --> b7["Impossible"]
Figure 2: What is achievable, what depends on the app type, and what is not achievable.
1.1 Terms Used in This Article
First, a quick pass over the words that come up repeatedly below.
| Term | Expansion | What it means in this article |
|---|---|---|
| UCRT | Universal C Runtime | The standard C library portion that was split out of the C runtime in Visual Studio 2015. On Windows 10 and later it ships as a component of the OS |
| VC++ redistributable package | Visual C++ Redistributable | An installer that puts the Visual C++ runtime DLLs other than the UCRT onto the target machine |
| framework-dependent | Deployment that depends on the .NET in the target environment | A deployment form that assumes the .NET runtime is already installed on the target machine |
| self-contained | .NET runtime bundled | A form in which the app carries a full copy of the .NET runtime |
| single-file | Single-file publish | The .NET publish option that consolidates the distribution into one EXE |
| Native AOT | Native Ahead-Of-Time | A deployment form that compiles IL to native code ahead of time at publish. It does not use a JIT at run time |
| app-local deployment | Application-adjacent deployment | Placing DLLs in the same folder as the EXE so that only that app uses them |
| xcopy deployment | Copy-only deployment | Deployment that uses neither an installer nor administrator rights: drop the folder in place and it runs |
| SCM | Service Control Manager | The OS mechanism that manages registration, start, and stop of Windows services |
| UAC | User Account Control | The Windows security mechanism that controls elevation to administrator rights |
| Shell extension | Shell extension | A COM component that is loaded into processes such as File Explorer and runs there |
| Evergreen | Continuously updated mode | The distribution mode that leaves the WebView2 Runtime to a single shared copy that updates itself |
| Fixed Version | Pinned-version mode | The distribution mode that bundles a specific version of the WebView2 Runtime with your app |
| arch | architecture, CPU architecture | x86 / x64 / Arm64 |
Knowledge map for this article
For a Windows app, collecting the deliverable into a single EXE is largely feasible in itself, and self-contained, single-file, and Native AOT in .NET and static linking (/MT) in C/C++ are available, but dependencies on the OS version, the architecture, system DLLs, and the security model remain, so the dependency on the target Windows cannot be reduced to zero. self-contained and framework-dependent are mutually exclusive choices, and Native AOT effectively behaves as single-file at the cost of no longer supporting features such as built-in COM. WebView2 requires choosing how the runtime is supplied, either Evergreen or Fixed Version, and for WinUI 3 whether PublishSingleFile is possible depends on whether the app is packaged (MSIX) or unpackaged. For Shell extensions and drivers the real subject is the design of registration and signing, so app-local distribution is often easier to maintain than forcing everything into one EXE.
flowchart LR
accTitle: Single-binary packaging for Windows apps and OS dependencies
accDescr: Diagram showing that reducing the deliverable to a single file and removing the dependency on the OS are separate axes, the single-file options available in .NET and in C/C++, and how host integrations such as WebView2 and WinUI 3 constrain the distribution format itself
single_binary_packaging["Single-Binary Packaging"]
windows_os_dependency["Target Windows Platform Dependency"]
self_contained_deployment["Self-Contained Deployment"]
framework_dependent_deployment["Framework-Dependent Deployment (.NET)"]
native_aot["Native AOT"]
builtin_com_interop["Built-in COM Interop"]
single_file_publish["Single-File Publish (.NET)"]
static_linking_crt["Static CRT Linking (/MT)"]
dynamic_linking_crt["Dynamic CRT Linking (/MD)"]
vc_redistributable["Visual C++ Redistributable"]
ucrt["UCRT (Universal C Runtime)"]
msix_packaging["MSIX Packaging"]
winui3["WinUI 3 / Windows App SDK"]
webview2["Microsoft Edge WebView2"]
webview2_runtime["WebView2 Runtime"]
webview2_evergreen["Evergreen Distribution Mode (WebView2)"]
webview2_fixed_version["Fixed Version Runtime (WebView2)"]
shell_extension["Shell Extension (Explorer Extension)"]
com["COM (Component Object Model)"]
driver_signing["Driver Signing"]
driver_package["Driver Package"]
app_local_deployment["App-Local Deployment"]
single_binary_packaging -->|"requires"| windows_os_dependency
self_contained_deployment -->|"incompatible with"| framework_dependent_deployment
native_aot -->|"incompatible with"| builtin_com_interop
single_file_publish -.->|"requires"| windows_os_dependency
native_aot -.->|"requires"| windows_os_dependency
static_linking_crt -->|"incompatible with"| dynamic_linking_crt
dynamic_linking_crt -->|"requires"| vc_redistributable
static_linking_crt -->|"uses"| ucrt
dynamic_linking_crt -->|"uses"| ucrt
msix_packaging -.->|"incompatible with"| single_file_publish
winui3 -.->|"uses"| msix_packaging
webview2 -->|"requires"| webview2_runtime
webview2_runtime -->|"configured by"| webview2_evergreen
webview2_runtime -->|"configured by"| webview2_fixed_version
webview2_evergreen -->|"incompatible with"| webview2_fixed_version
shell_extension -->|"uses"| com
driver_signing -->|"requires"| driver_package
driver_package -->|"not recommended for"| single_binary_packaging
app_local_deployment -->|"recommended for"| single_binary_packaging
self_contained_deployment -.->|"requires"| windows_os_dependency
single_binary_packaging -.->|"uses"| static_linking_crt
webview2 -.->|"requires"| windows_os_dependency
In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (22 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle
2. Think of “Single Binary” in Four Levels
Here are those levels as a diagram first. They get harder from the bottom up, and the top one is unreachable on Windows.
flowchart BT
A["Level A: One distribution artifact<br/>Largely achievable"]
B["Level B: No pre-installed language runtime<br/>Largely achievable"]
C["Level C: No installation or registration<br/>Depends on the kind of app"]
D["Level D: No dependency on the target Windows<br/>Unreachable on Windows"]
A --> B
B --> C
C --> D
Figure 3: The four levels of a single binary. They get harder from the bottom up, and Level D is unreachable.
Which of these four levels someone means by “we want a single binary” completely changes the work involved.
2.1 Level A: One distribution artifact
The most superficial level is this.
- You can email a single file
- You only need to put one file on a USB stick
- You only place
app.exeat the destination
This is about the apparent distribution unit. Even if the app extracts files temporarily at startup, or depends on OS-side DLLs, this condition alone can still be satisfied.
2.2 Level B: No pre-installed language runtime required
Next is the state where the app runs without pre-installing the .NET runtime or the VC++ redistributable package on the target machine.
- Static linking for C/C++
- .NET self-contained
- .NET single-file
- .NET Native AOT
At this level, the feeling of “I can carry this around on its own” gets much stronger.
2.3 Level C: No installation or registration required
From here, things suddenly get hard.
A plain EXE may run just by being dropped in place. But these are a different story.
- Shell extensions
- Windows services
- Custom URL schemes and file associations
- Drivers
- Components loaded into other processes, such as Explorer or Office
This territory is not satisfied by just placing a file. It requires registration on the OS side, or wiring into a host.
flowchart TB
accTitle: The territory where dropping files in place is not enough
accDescr: A diagram showing that shell extensions, Windows services, file associations, drivers, and components loaded into other processes are not satisfied by placing files, and require registration on the OS side or wiring into a host.
c1["Shell extensions and services"] --> c4["Placing files is not enough"]
c2["Associations and drivers"] --> c4
c3["Things loaded into other processes"] --> c4
c4 --> c5["OS-side registration and host wiring are required"]
Figure 4: Level C gets hard all at once because it is the territory that requires registration with the OS.
2.4 Level D: No dependency on the target Windows
This is impossible on Windows.
A Windows app ultimately runs on top of Windows APIs, the loader, the security model, and the device stack. Single-binary packaging only covers the app’s own area of responsibility. You are not taking the OS itself along with you.
3. The Areas Where a Single EXE Is Quite Achievable
Even on Windows, some apps lend themselves relatively well to a single EXE.
- Desktop tools launched standalone
- Business apps where the EXE itself owns the UI and the processing
- Tools for communication, file processing, log collection, monitoring, or equipment control
- Things that do not require host integration with Explorer or Office
- UIs that do not assume a web runtime
For this type, many things are easy to fold into the app itself.
- Your own code
- Resources
- The manifest
- Default settings
- Template data
- Some third-party libraries
- The language runtime itself
Furthermore, even without fully embedding DLLs inside the EXE, app-local deployment, placing DLLs next to the EXE, is a perfectly viable mainstream option on Windows. In practice,
- a single
app.exe - or
app.exeplus a few adjacent DLLs - with no installer, no administrator rights, distributable via xcopy
this shape is often easier to maintain than forcing everything into one EXE.
flowchart TB
accTitle: app-local deployment as the practical landing point
accDescr: A diagram showing that even without fully embedding DLLs into the EXE, a shape that places DLLs next to the EXE and can be xcopy-deployed with no installer and no administrator rights is often easier to maintain than compressing everything into one EXE.
d1["app.exe plus a few adjacent DLLs"] --> d2["No installer, no administrator rights"]
d2 --> d3["Deployable with xcopy"]
d3 -.-> d4["Often easier to maintain than a forced single EXE"]
Figure 5: You can often meet the goal with app-local deployment without insisting on one EXE.
4. The Windows Dependencies That Remain Even With One EXE
If you assume “one EXE means no dependency on the target Windows,” this is where you get hurt. In reality, dependencies remain even with a single EXE.
4.1 OS version dependency
Each Windows API has a minimum supported OS. There are also x64 / Arm64 differences. So even with a single EXE, you need to pin down from the start:
- Does it run down to Windows 10?
- Is Windows 11 assumed?
- Does it also need to run on Windows Server?
- Which of x86 / x64 / Arm64 are targeted?
4.2 System DLL dependency
Even when you think you have a single EXE, at runtime you are of course using OS-provided components.
kernel32.dlluser32.dlladvapi32.dll- The COM infrastructure
- The service control infrastructure
These are Windows’ area of responsibility.
4.3 Security model dependency
- UAC
- File ACLs
- The Service Control Manager
- The registry
- Driver signing policy
Things like these cannot be absorbed into the app alone.
flowchart TB
accTitle: Dependencies that remain even with a single EXE
accDescr: A diagram showing that even with a single EXE, dependencies remain on the minimum supported OS and architecture of the Windows APIs, on system DLLs such as kernel32.dll and the COM infrastructure, and on the security model including UAC, ACLs, and driver signing policy.
e0["A single-EXE app"] --> e1["OS version and arch"]
e0 --> e2["System DLLs and the COM infrastructure"]
e0 --> e3["The security model"]
e1 --> e4["None of these can be absorbed by the app"]
e2 --> e4
e3 --> e4
Figure 6: Consolidating into one artifact leaves the dependency on Windows’ area of responsibility untouched.
4.4 Host and runtime dependency
If the design is not a standalone EXE but something that rides on a host, dependencies multiply at once.
- Using WebView2: the WebView2 Runtime is needed
- Using WinUI 3 / Windows App SDK: the deployment mode needs sorting out
- Building a shell extension: registration on the Explorer side is needed
In other words, your choice of UI or integration often becomes, directly, your distribution difficulty.
flowchart TB
accTitle: UI and integration choices become distribution difficulty
accDescr: A diagram showing that using WebView2 requires the WebView2 Runtime, using WinUI 3 requires sorting out the deployment mode, and building a shell extension requires registration with Explorer, so that the choice of host or runtime becomes the distribution difficulty itself.
f1["Use WebView2"] --> f2["The Runtime is required"]
f3["Use WinUI 3"] --> f4["The deployment mode must be sorted out"]
f5["Build a shell extension"] --> f6["Registration with Explorer is required"]
Figure 7: The further you move from a standalone EXE, the faster dependencies pile up.
5. Realistic Trade-Offs by Technology
5.1 Native C/C++
Native C/C++ sits on the high-freedom side of single-binary packaging. Static linking is an option, and a standalone EXE can be consolidated quite far.
That said, more important in practice than cramming everything into one file are:
- What to do about the UCRT and the VC++ runtime
- Whether to place third-party DLLs app-local
- How tightly to narrow the target CPU / OS
Starting point: the difference between /MT and /MD
In MSVC, what actually decides “do we carry the runtime ourselves” is this compiler option.
| Option | What gets linked | What the target machine needs |
|---|---|---|
/MD |
The DLL import libraries ucrt.lib and vcruntime.lib. At run time it uses ucrtbase.dll and vcruntime<version>.dll |
The UCRT and the VC++ runtime |
/MT |
libucrt.lib, libvcruntime.lib, and libcmt.lib, linked statically |
No additional runtime |
The smallest way to try it is this single line in a Developer Command Prompt.
cl /std:c++17 /EHsc /O2 /MT app.cpp /Fe:app.exe
There are three things to watch here.
- When the C runtime was split apart in Visual Studio 2015, the UCRT became a component of Windows, and it ships as part of the OS on Windows 10 and later. If you target anything older than that, you need to redistribute it with
vcredist. - Building a DLL with
/MTis not recommended. A statically linked CRT keeps its state inside that DLL alone, so memory allocation, the locale, and the reach of_set_se_translatordiverge between the EXE and the DLL. Sloppily matching them up as “the EXE is/MT, so the bundled DLLs are/MTtoo” leads to crashes when an allocation and its free cross the boundary. /clrand/MTcannot be combined. If C++/CLI is in the mix, you are on the/MDside.
flowchart TB
accTitle: Three cautions about static linking
accDescr: A diagram showing that the UCRT ships with the OS on Windows 10 and later but needs vcredist redistribution on older Windows, that building a DLL with /MT confines the CRT state inside the DLL and causes crashes when allocation and free cross the boundary, and that /clr cannot be combined with /MT.
g0["Static linking with /MT"] --> g1["Older Windows needs vcredist"]
g0 --> g2["/MT on a DLL is discouraged"]
g0 --> g3["Cannot be combined with /clr"]
g2 -.-> g4["Crashes when allocation and free cross the boundary"]
Figure 8: /MT is powerful, but it needs care on three points: the UCRT assumption, DLL boundaries, and C++/CLI.
5.2 .NET
.NET offers single-file, self-contained, and Native AOT, so the apparent distribution unit can be made quite small.
But you need to keep the distinctions straight.
- framework-dependent: depends on the .NET installed in the target environment
- self-contained: carries the .NET runtime with it
- single-file: consolidates the distribution into one artifact
- Native AOT: further reduces startup-time dependencies, but comes with feature constraints
“Single-file” does not mean “fewer OS dependencies.” What it mainly reduces is how scattered the app’s distribution artifacts are.
flowchart TB
accTitle: What single-file reduces is the scatter of distribution artifacts
accDescr: A diagram showing that framework-dependent relies on the .NET in the target environment, self-contained carries the runtime, and single-file only consolidates the distribution into one artifact, so single-file does not by itself reduce OS dependencies.
h1["framework-dependent"] --> h2["Depends on the .NET in the target environment"]
h3["self-contained"] --> h4["Carries the .NET runtime"]
h5["single-file"] --> h6["Consolidates into one artifact"]
h6 -.-> h7["Does not reduce OS dependencies"]
Figure 9: Each .NET deployment form reduces a different thing and leaves a different thing behind.
Starting point: the three dotnet publish patterns
To just run them and see the difference, these three lines are enough.
rem 1. framework-dependent + single-file: the target machine needs the .NET runtime
dotnet publish -c Release -r win-x64 --self-contained false -p:PublishSingleFile=true
rem 2. self-contained + single-file: carries the .NET runtime
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true
rem 3. Native AOT: publish after writing PublishAot in the csproj
dotnet publish -c Release -r win-x64
Microsoft officially recommends writing PublishSingleFile and PublishAot in the csproj rather than on the command line. That enables compatibility analysis during the build, which cuts down on problems you would otherwise only notice after publishing.
<PropertyGroup>
<PublishSingleFile>true</PublishSingleFile>
<PublishAot>true</PublishAot>
</PropertyGroup>
Note that specifying RuntimeIdentifier makes SelfContained default to true. When you want framework-dependent, pass --self-contained false explicitly. Publishing Native AOT on Windows requires the Desktop development with C++ workload in Visual Studio.
Trade-off: “making it one” is not free
Writing only about what is possible here would trip people up, so here is the price alongside it.
Startup-time extraction with single-file
- By default, only managed DLLs are bundled. They are loaded into memory at startup and are not extracted to a folder. The native binaries of the runtime itself stay as separate files.
- If you want the native binaries in the single file too, use
IncludeNativeLibrariesForSelfExtract; if you want everything extracted before execution, useIncludeAllContentForSelfExtract. Both switch the behavior to extract-then-start, and on Windows the extraction goes under%TEMP%\.net. You can change the location withDOTNET_BUNDLE_EXTRACT_BASE_DIR, but do not point it somewhere writable by users or services with different privileges. - Enabling
EnableCompressionInSingleFilemakes the EXE considerably smaller, but startup gets slower because of the in-memory decompression. Microsoft’s own wording is to measure both size and startup cost before using it. The impact varies widely by app.
API incompatibilities with single-file
Once the distribution is a single artifact, code that assumes file paths breaks quietly.
| API | Behavior under single-file |
|---|---|
Assembly.Location |
Returns an empty string |
Assembly.CodeBase |
PlatformNotSupportedException |
Assembly.GetFile |
IOException |
Module.Name |
Returns the string <Unknown> |
To touch files next to the EXE, move to AppContext.BaseDirectory; when you need the path of the executable, move to Environment.ProcessPath. Third-party libraries sometimes use these internally, so once you go single-file you need to launch it at least once on a real machine and confirm it works.
flowchart TB
accTitle: Path-dependent code breaks quietly
accDescr: A diagram showing that consolidating the distribution into one artifact changes the behavior of path-dependent APIs such as Assembly.Location returning an empty string, so files next to the EXE should use AppContext.BaseDirectory, the executable path should use Environment.ProcessPath, and the app should be launched at least once on a real machine.
i1["Go single-file"] --> i2["Path-dependent API behavior changes"]
i2 --> i3["Files next to the EXE: AppContext.BaseDirectory"]
i2 --> i4["The executable path: Environment.ProcessPath"]
i3 --> i5["Launch once on a real machine to confirm"]
i4 --> i5
Figure 10: Once you go single-file, swap out how you obtain paths and confirm it on a real machine.
Feature constraints of Native AOT
In exchange for “fast startup, no runtime required,” the following become unavailable.
- Dynamic loading such as
Assembly.LoadFile - Runtime code generation such as
System.Reflection.Emit - C++/CLI
- Built-in COM on Windows
- Trimming is mandatory, so the trimming constraints come along as well
- It is effectively single-file, so the API incompatibilities above come along too
System.Linq.Expressionsalways runs interpreted, which is slower than runtime-generated code
The target platforms are fixed in advance as well. On .NET 8, Windows means x64 and Arm64; x86 was added in .NET 9. The idea of “one AnyCPU artifact” does not hold from the outset.
flowchart TB
accTitle: The price of Native AOT
accDescr: A diagram showing that Native AOT reduces startup-time dependencies but gives up dynamic loading, runtime code generation, C++/CLI, and built-in COM on Windows, carries the trimming and single-file constraints as well, and fixes the target platform in advance.
j0["Native AOT"] --> j1["Reduces startup-time dependencies"]
j0 --> j2["No dynamic loading or runtime generation"]
j0 --> j3["No C++/CLI, no built-in COM"]
j2 -.-> j4["Trimming and single-file constraints apply too"]
j3 -.-> j5["Target OS and arch are fixed in advance"]
Figure 11: Look at the benefits of Native AOT together with the features you lose.
5.3 WebView2
Adopting WebView2 completely changes the single-binary difficulty. The real subject here is not the number of EXEs but how you handle the WebView2 Runtime.
Before “can we make it one EXE,” there are questions to answer first.
- Do you assume the Runtime already exists in the environment?
- Do you use Evergreen?
- Do you bundle a Fixed Version?
- How much responsibility do you take for offline distribution?
Roughly, the difference between Evergreen and Fixed Version is this.
| Evergreen | Fixed Version | |
|---|---|---|
| Who updates it | Microsoft. It updates automatically | You. You swap it in along with your app updates |
| What sits on the machine | A single copy shared by all apps | Bundled per app |
| Distribution size | Barely grows | Over 250 MB |
| How it gets installed | The Bootstrapper is about 2 MB and downloads what it needs. For offline, bundle the Standalone Installer | Ship the extracted set of binaries together with your app |
| Existing constraints | You have to check whether it is on the machine before launch | It cannot be run from a network path or a UNC path |
The Evergreen Runtime comes preinstalled as part of the OS on Windows 11. Some Windows 10 machines still do not have it, though, so Microsoft itself words it as “even if you choose Evergreen, you should still distribute the Runtime.” In other words, checking from the app whether it is present and installing it if not is a step you need either way. Fixed Version gives you control over when updates happen, at the cost of adding several hundred MB to what you ship. This has to be decided up front, separately from the single-EXE question.
flowchart TB
accTitle: Decide the WebView2 distribution method first
accDescr: A diagram showing that Evergreen is a single shared copy updated automatically by Microsoft but may be absent from a machine, that Fixed Version puts updates under your control at the cost of several hundred MB, and that either way the app needs a step that checks for the Runtime and installs it if missing.
k0["Adopt WebView2"] --> k1["Evergreen: one shared copy, auto-updated"]
k0 --> k2["Fixed Version: bundle and update it yourself"]
k1 --> k3["Check for it and install if missing"]
k2 -.-> k4["Adds several hundred MB to the distribution"]
Figure 12: The real subject with WebView2 is not the number of EXEs but how the Runtime is handled.
5.4 WinUI 3 / Windows App SDK
WinUI 3 also changes your distribution requirements the moment you adopt it. Choosing the UI technology is, in itself, choosing the distribution method.
Concretely, there are two axes to decide.
- packaging: packaged (MSIX), packaged with external location, or unpackaged
- runtime: framework-dependent or self-contained
And from a single-binary point of view, this combination constraint is what bites.
PublishSingleFileis available only for WinUI 3 apps that are unpackaged and self-contained, and it requires Windows App SDK 1.5 or later.PublishSingleFilecannot be used for packaged apps, nor for packaged apps with external location.- Going unpackaged means losing package identity, so Windows features that depend on package identity, such as notifications, background tasks, file associations, and context menu extensions, become unavailable.
In other words, with WinUI 3, “make it one EXE” and “use the Windows extension features” collide head-on. If a single binary is the top priority, it is often faster to revisit the UI technology assumption first.
flowchart TB
accTitle: One EXE collides with package identity
accDescr: A diagram showing that PublishSingleFile in WinUI 3 works only for an unpackaged and self-contained configuration, that going unpackaged loses package identity, and that notifications and file associations then become unavailable, so a single EXE collides head-on with the Windows extension features.
m1["Want to use PublishSingleFile"] --> m2["Only unpackaged plus self-contained"]
m2 --> m3["Loses package identity"]
m3 --> m4["No notifications, no associations"]
m4 -.-> m5["One EXE collides head-on with the extension features"]
Figure 13: In WinUI 3, choosing one EXE means giving up package identity.
6. Areas That Inherently Require Registration and Dependencies
6.1 Shell extensions
A shell extension loaded into Explorer is a different animal from a “drop-in EXE.” Here the real subject is not the file count but how you register with Explorer.
6.2 Windows services
Even if the service executable itself can be a single file, distribution is a separate problem.
- Registration with the SCM
- Privileges
- The logon account
- Recovery settings
all need consideration. In other words, services are an area where you nail down “how to install” rather than “how to make it one EXE.”
6.3 Drivers
Drivers are even more clear-cut. They only come together as a package including the INF, signing, and the installation procedure, so they barely get onto the single-binary playing field in the first place.
flowchart TB
accTitle: The areas where registration and signing are the real subject
accDescr: A diagram showing that shell extensions need registration with Explorer, Windows services need SCM registration plus privileges and a logon account, and drivers only come together with the INF file and signing, so the design of registration and dependencies matters more than the file count.
n1["Shell extensions"] --> n2["Registration with Explorer"]
n3["Services"] --> n4["SCM registration, privileges, account"]
n5["Drivers"] --> n6["INF and signing"]
n4 -.-> n7["Registration design matters more than file count"]
Figure 14: In these three areas, nail down how to register rather than how to make it one EXE.
7. A Decision Table for Practice
For a rough judgment, this table is handy.
| What you want to build | Single-EXE feasibility | What to think about first |
|---|---|---|
| Standalone Win32 / C++ tool | High | Static linking, target OS / arch |
| Standalone WinForms / WPF tool | High | Suitability of self-contained, single-file, Native AOT |
| WinUI 3 / Windows App SDK app | Medium | Deployment mode, additional dependencies |
| WebView2-based desktop UI | Low to medium | How the Runtime is distributed |
| Explorer context-menu extension or preview handler | Low | COM / registry registration |
| Windows service | Medium | SCM registration, privileges, update procedure |
| App that bundles a driver | Low | INF, signing, installation |
The most important takeaway from this table is that “the number of binaries” and “the scope of distribution responsibility” are different things.
8. What to Decide First in Distribution Design
If you want single-binary packaging to succeed, there are things to decide before implementation.
8.1 Decide what you actually want to be “one”
- Do you want one distribution artifact?
- Do you want to eliminate runtime pre-installation?
- Do you want to eliminate the installer?
- Do you want to make offline updates easy?
The answer determines which technology you choose.
8.2 Pin down the minimum supported Windows and arch first
Both single-file and Native AOT are fundamentally OS / architecture specific. If you leave this vague and push ahead with “just make it one file,” you get stuck at the end with missing APIs or runtime mismatches.
8.3 Write down what you bundle and what you leave to Windows
In practice, just writing out this table means far fewer things go wrong.
- What the app bundles
- The main exe
- Your own DLLs
- Settings templates
- The self-contained runtime
- What you leave to Windows
- System DLLs
- OS APIs
- SCM / registry / Explorer
- The driver infrastructure
- What you assume as separate prerequisites
- WebView2 Runtime
- VC++ Redistributable
- Office / Excel
- Dedicated drivers
flowchart TB
accTitle: Write down the three categories of responsibility
accDescr: A diagram showing that simply writing down the three categories of what the app bundles, what is left to Windows, and what is assumed as a separate prerequisite means far less goes wrong when you distribute the app.
p0["Scope of distribution responsibility"] --> p1["What the app bundles"]
p0 --> p2["What is left to Windows"]
p0 --> p3["What is assumed separately"]
p3 -.-> p4["Writing it down alone means less goes wrong"]
Figure 15: Write down the three categories of bundled, left to the OS, and assumed separately, before you start.
8.4 If you prioritize a single binary, reduce host integration
This one is quite effective.
- Drop the shell extension and make it a plain EXE
- Skip running as a service; use Task Scheduler or explicit launch instead
- Use a native UI instead of WebView2
- Keep COM closed within your own process
In short, the more you reduce designs that have the OS “load” or “register” your code, the closer you get to a single binary.
flowchart TB
accTitle: The less host integration, the closer you get
accDescr: A diagram showing that dropping the shell extension for a plain EXE, avoiding a service in favor of Task Scheduler or explicit launch, and using a native UI instead of WebView2 all reduce designs that ask the OS to load and register your code, which brings you closer to a single binary.
q1["Drop the shell extension for a plain EXE"] --> q4["Fewer designs that register with the OS"]
q2["No service, use explicit launch"] --> q4
q3["Native UI instead of WebView2"] --> q4
q4 --> q5["Closer to a single binary"]
Figure 16: If a single binary is the priority, reduce the host integration itself.
9. Summary
Single-binary packaging on Windows is feasible to a considerable extent. But it all comes down to this one statement:
You can make the app a single EXE. But you cannot make the Windows the app depends on into a single EXE.
Five points worth remembering in particular:
- For an ordinary standalone EXE, you can push single-file distribution remarkably far
- Static linking for C/C++, .NET single-file, and Native AOT are strong options
- However, dependencies on the OS version, arch, system DLLs, and the security model do not disappear
- For shell extensions, services, drivers, WebView2, and parts of WinUI 3, OS registration and additional runtimes become the main story
- Whether single-binary succeeds is decided by separating, at the start, what exactly you want to be one
If you strongly prioritize a single binary, designing to lower the coupling with the OS at technology-selection time makes success far more likely.
flowchart TB
accTitle: The key to success is coupling with the OS
accDescr: A diagram showing that while the app can be made a single EXE, the Windows it depends on cannot, so if a single binary is a strong priority it is more likely to succeed when the design lowers coupling with the OS at technology-selection time.
r1["Make the app a single EXE"] --> r2["Achievable"]
r3["Make the Windows it depends on a single EXE"] --> r4["Not achievable"]
r4 -.-> r5["So lower the coupling with the OS at selection time"]
Figure 17: Whether one-EXE packaging works is decided by how you design coupling with the OS at the start.
10. References
- Microsoft Learn, Create a single file for application deployment
- Microsoft Learn, Native AOT deployment overview
- Microsoft Learn, C runtime (CRT) and C++ standard library (STL) lib files
- Microsoft Learn, Dynamic-link library search order
- Microsoft Learn, Targeting your application for Windows
- Microsoft Learn, Creating Registration-Free COM Objects
- Microsoft Learn, Registering Shell Extension Handlers
- Microsoft Learn, CreateServiceW function
- Microsoft Learn, Overview of INF Files
- Microsoft Learn, Windows driver signing tutorial
- Microsoft Learn, Distribute your app and the WebView2 Runtime
- Microsoft Learn, Windows App SDK deployment guide for self-contained apps
- Microsoft Learn, Package and deploy Windows apps overview
- Microsoft Learn, Packaging overview - Windows apps
- Microsoft Learn, Evergreen vs. fixed version of the WebView2 Runtime
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
Windows App Outsourcing and Custom Software Development: What to Sort Out Before You Ask
Before commissioning Windows app outsourcing or custom software development, here is how to sort out existing software modification, devi...
Is WebView2 the Right Successor to IE Mode? — The ActiveX Constraint and a Realistic Migration Design
A rundown of WebView2's basic architecture, the Evergreen vs. Fixed Version distribution strategies, the user data folder trap, how nativ...
What Is ClickOnce? - How It Works, How It Updates, and Where It Fits (and Doesn't) in Practice
An overview of ClickOnce, the deployment technology used for .NET Windows desktop apps - manifests, updates, the cache, signing, and whic...
A Checklist for Safely Handling Child Processes in Windows Apps
Handling child processes safely in a Windows app depends less on the launch API than on who owns the process tree and how shutdown is des...
The Win32 Thread Pool API — Concurrency Without Creating Threads, via CreateThreadpoolWork
Are you spawning CreateThread calls all over your native code? This article explains the Win32 thread pool API redesigned in Vista — the ...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
For Windows app distribution, you can avoid rework by designing single-file packaging, runtime bundling, the decision to adopt WebView2 or WinUI, and whether to run as a service, all together.
Technical Consulting & Design Review
The request 'we want a single EXE' becomes much easier to decide on once you separate the distribution unit, OS dependencies, registration requirements, and update responsibility.
Frequently Asked Questions
Common questions about the topic of this article.
- Can a Windows app be distributed as a single EXE file and nothing else?
- For a desktop tool that is launched on its own, you can get quite far. Static linking in C/C++, .NET self-contained and single-file, and Native AOT all let you consolidate the distribution into one EXE. But being able to produce a single EXE and being independent of the target Windows are two different things: dependencies on the OS version, the CPU architecture, system DLLs, and the security model do not go away.
- Does .NET single-file eliminate OS dependencies?
- No. What single-file mainly reduces is how scattered the app's distribution artifacts are, not the OS dependencies. Framework-dependent deployment relies on the .NET installed in the target environment, self-contained carries the .NET runtime with it, and Native AOT reduces startup-time dependencies further but comes with feature constraints. Both single-file and Native AOT are fundamentally OS- and architecture-specific, so you have to pin down the minimum supported Windows and the arch at the very start.
- Which kinds of apps are hard to ship as a single EXE?
- Shell extensions, Windows services, drivers, WebView2-based UIs, and parts of WinUI 3. For these the real subject is registration with the OS and the handling of additional runtimes rather than the file count. A shell extension has to be registered with Explorer, a service needs registration with the SCM plus a design for privileges and the logon account, and a driver only comes together once the INF file and signing are included, so drop-in deployment simply does not work. With WebView2 you first have to decide how the WebView2 Runtime is distributed.
- Is there a better distribution method than forcing everything into one EXE?
- App-local deployment, placing DLLs next to the EXE, is a strong option. Even a shape like app.exe plus a few adjacent DLLs is often easier to maintain than compressing everything into one EXE, as long as it can be xcopy-deployed with no installer and no administrator rights. What matters is separating, at the start, whether you want one distribution artifact, no runtime pre-installation, or no installer at all.