Windows 应用的单文件分发 - 单一可执行文件与系统依赖的界限
· 小村 豪 · Windows, 分发, 单一可执行文件, .NET, C++, WebView2, WinUI
这篇文章的起点,是下面这条帖子。
在 Windows 上,「如果可以,希望只用一个文件分发」是相当普遍的需求。对于内部工具、设备联动工具、监控端、离线环境,以及尽量想避开安装程序的现场,做成单一可执行文件 都很有吸引力。
不过,这个话题如果不先分清楚,讨论往往会在中途就对不上。因为在 Windows 上说「想做成单一二进制文件」时,其实很容易混进下面 4 种不同的需求。
- 想让发布物只有 1 个
- 想不用事先安装 .NET 或 Visual C++ 的运行时
- 想不用安装程序、不需要管理员权限,放着就能跑
- 想不受目标 Windows 差异的影响
这 4 件事并不相同。实务上比较不容易出偏差的想法是:
把发布物合并成 1 个 EXE,是相当可行的。 但要把对目标 Windows 的依赖完全消除,是做不到的。
本文将从 Windows 应用实务的角度,把这条界线梳理清楚。
1. 先说结论
先直接把结论列出来。
- 普通的桌面 EXE,可以做到相当高程度的单一可执行文件化
- 但 能做成 1 个 EXE 和 不依赖目标 Windows,是两件不同的事
- Shell 扩展、Windows 服务、驱动、WebView2、部分 WinUI 3 场景,重点往往不在文件数量,而在于 要向系统注册什么、以什么为前提
- 实务上最重要的是分清楚:到底想做单一可执行文件、想去掉安装程序,还是想降低系统依赖
换句话说,在 Windows 上,这条界线大致是这样的:
- 把发布物合并成 1 个:相当可行
- 把额外运行时一并带上:相当可行
- 做到 xcopy 分发:取决于应用类型
- 消除对目标 Windows 的依赖:不可能
2. 把「单一可执行文件」拆分成 4 个层次
2.1 层次 A:发布物只有 1 个
最表面的一层是这样的:
- 用邮件发 1 个附件就能寄出
- U 盘里放 1 个文件就够
- 目标机器上只放一个
app.exe
这只是发布单位的外观。实际上它可能在启动时临时解压,也可能依赖系统侧的 DLL,但仅就这一条件而言,是能够做到的。
2.2 层次 B:无需预先安装语言运行时
接下来是无需事先在目标机器上安装 .NET 运行时或 VC++ 可再发行组件包:
- C/C++ 的静态链接
- .NET 的 self-contained
- .NET 的 single-file
- .NET Native AOT
到了这一层,「可以单独带走」的感觉就会明显强很多。
2.3 层次 C:无需安装或注册
从这里开始会突然变难。
单纯的 EXE 或许放着就能跑,但下面这些属于另一类:
- Shell 扩展
- Windows 服务
- 自定义 URL scheme 或文件关联
- 驱动
- 会被 Explorer、Office 等其他进程加载的组件
这一类光靠放文件是不够的,需要向系统注册,或者与宿主进行对接。
2.4 层次 D:不依赖目标 Windows
这一层在 Windows 上是做不到的。
因为 Windows 应用最终仍然要跑在 Windows 的 API、加载器、安全模型、设备栈之上。单一可执行文件所能覆盖的范围,只到应用自身的责任范围,并不包括把操作系统本身也一起带走。
3. 比较容易做成单个 EXE 的领域
在 Windows 上,以下类型的应用相对容易合并成 1 个 EXE。
- 单独启动的桌面工具
- EXE 本身同时承担 UI 和处理逻辑的业务应用
- 通信、文件处理、日志收集、监控、设备控制这类工具
- 不需要与 Explorer、Office 做宿主整合的应用
- 不以 Web 运行时为前提的 UI
这类应用中,以下内容通常都可以比较轻松地纳入本体:
- 自有代码
- 资源文件
- manifest
- 默认设置
- 模板数据
- 部分第三方库
- 语言运行时本体
此外,即使没有把 DLL 完全嵌入 EXE,把 DLL 放在 EXE 旁边的 app-local 分发方式,在 Windows 上依然相当实用。实务中,
- 单独一个
app.exe - 或者
app.exe+ 少量相邻 DLL - 但无需安装程序、无需管理员权限、可以直接 xcopy 分发
这种形式往往比硬要压缩成 1 个 EXE 更容易维护。
4. 即使做成 1 个 EXE 也无法消除的系统依赖
如果认为「只要 EXE 只有 1 个,就不再依赖目标 Windows」,就会在这里栽跟头。实际上,即便合并成了 1 个 EXE,以下依赖仍然存在。
4.1 操作系统版本依赖
Windows API 都各自有最低支持的系统版本,x64 / Arm64 之间也存在差异。也就是说,即使打包成单个 EXE,仍然需要先确定:
- 是否要支持到 Windows 10
- 是否以 Windows 11 为前提
- 是否也要在 Windows Server 上运行
- 目标是 x86 / x64 / Arm64 中的哪一种
4.2 系统 DLL 依赖
即便你觉得已经做成了 1 个 EXE,运行时当然仍会用到系统提供的组件:
kernel32.dlluser32.dlladvapi32.dll- COM 基础设施
- 服务控制基础设施
这些属于 Windows 侧的责任范围。
4.3 安全模型依赖
- UAC
- 文件 ACL
- 服务控制管理器(SCM)
- 注册表
- 驱动签名策略
这些是应用无法独自承担的部分。
4.4 宿主与运行时依赖
如果不是单独启动的 EXE,而是要运行在某个宿主之内的设计,依赖会一下子增多:
- 使用 WebView2:需要 WebView2 Runtime
- 使用 WinUI 3 / Windows App SDK:需要理清分发模式
- 制作 Shell 扩展:需要向 Explorer 注册
也就是说,UI 和整合方式的选择,会直接决定分发的难度。
5. 按技术类型看现实的落地点
5.1 原生 C/C++
原生 C/C++ 是单一可执行文件自由度较高的一侧,有静态链接的余地,单独启动的 EXE 相对容易合并。
不过,比起「把所有内容都压进一个文件」,实务上更重要的是:
- 如何处理 UCRT 和 VC++ 运行时
- 第三方 DLL 是否放在 app-local
- 目标 CPU / 系统要收窄到什么范围
5.2 .NET
.NET 拥有 single-file、self-contained、Native AOT,表面上的发布单位可以做得相当小。
但需要区分清楚:
- framework-dependent:仍依赖目标环境已安装的 .NET
- self-contained:把 .NET 运行时一并带上
- single-file:把发布物合并成一个
- Native AOT:进一步减少启动期依赖,但存在功能限制
「因为是 single-file,所以系统依赖会变少」这种说法并不成立。减少的主要是应用发布物的集合数量。
5.3 WebView2
一旦采用 WebView2,单一可执行文件的难度会截然不同。这里真正的重点不是 EXE 的数量,而是如何处理 WebView2 Runtime。
比起「能否做成 1 个 EXE」,更应该先问:
- 是否以现场已有的 Runtime 为前提
- 是否使用 Evergreen 分发
- 是否随附 Fixed Version
- 离线分发的责任要扛到什么程度
5.4 WinUI 3 / Windows App SDK
WinUI 3 也是一样,一旦采用,分发要求就会随之改变。UI 技术的选择,本身就等同于分发方式的选择。
如果单一可执行文件是最优先事项,先重新审视 UI 技术本身的前提,往往会更快找到答案。
6. 本质上就需要「注册与依赖」的领域
6.1 Shell 扩展
被 Explorer 加载的 Shell 扩展,与「放着就能跑的 EXE」本质上不同。这里的重点不是文件数量,而是如何向 Explorer 注册。
6.2 Windows 服务
即便服务本体的 exe 能合并成一个文件,分发依然是另一个问题:
- 向 SCM 注册
- 权限
- 启动账户
- 恢复设置
都需要设计。也就是说,服务这一类,重点在于「如何安装」,而不是「是否做成了 1 个 EXE」。
6.3 驱动
驱动更是明确:需要 INF 文件、签名、安装流程齐备才能成立,从一开始就很难站上单一可执行文件的擂台。
7. 实务判断表
粗略判断时,可以参考下表。
| 想做的东西 | 做成单个 EXE 的现实程度 | 应先考虑的事 |
|---|---|---|
| 单独启动的 Win32 / C++ 工具 | 高 | 静态链接、目标系统 / 架构 |
| 单独启动的 WinForms / WPF 工具 | 高 | self-contained、single-file、Native AOT 是否适用 |
| WinUI 3 / Windows App SDK 应用 | 中 | 分发模式、额外依赖 |
| 基于 WebView2 的桌面 UI | 低到中 | Runtime 的分发方式 |
| Explorer 右键扩展或预览处理程序 | 低 | COM / 注册表注册 |
| Windows 服务 | 中 | SCM 注册、权限、更新流程 |
| 附带驱动的应用 | 低 | INF、签名、安装 |
这张表里最重要的一点是:「二进制文件的数量」和「分发的责任范围」是两件不同的事。
8. 分发设计中应先决定的事
想让单一可执行文件真正落地,比起实现细节,先把下面这些事情理清楚会更顺利。
8.1 先决定「想把什么合并成 1 个」
- 是想让发布物只有 1 个
- 是想省掉运行时的预先安装
- 是想去掉安装程序
- 是想让离线更新更简单
答案不同,选择的技术也会不同。
8.2 先确定最低支持的 Windows 版本和架构
single-file 和 Native AOT 基本上都与系统版本和架构相关。如果这一点含糊不清,就硬推「总之先合并成一个文件」,最后往往会被 API 缺失或运行时不匹配卡住。
8.3 把「随附的内容」和「交给 Windows 处理的内容」写清楚
实务中,只要把下面这张表提前写出来,就能减少不少意外。
- 随应用一起分发的内容
- 本体 exe
- 自有 DLL
- 配置模板
- self-contained 运行时
- 交给 Windows 处理的内容
- 系统 DLL
- 系统 API
- SCM / 注册表 / Explorer
- 驱动基础设施
- 另外需要预先具备的内容
- WebView2 Runtime
- VC++ Redistributable
- Office / Excel
- 专用驱动
8.4 如果优先考虑单一可执行文件,就减少宿主整合
这一点相当有效。
- 放弃 Shell 扩展,改用普通 EXE
- 不做成服务,改用任务计划程序或显式启动
- 不使用 WebView2,改用原生 UI
- 把 COM 都封闭在自己的进程内
归根结底,越减少「让系统加载」「向系统注册」这类设计,就越接近单一可执行文件。
9. 总结
在 Windows 上实现单一可执行文件,能做到的范围相当大。但归根结底可以浓缩成一句话:
应用可以做成 1 个 EXE。 但这个应用所依赖的 Windows 本身,不可能也被合并成 1 个 EXE。
尤其要记住以下 5 点:
- 单独启动的普通 EXE,可以相当接近单文件分发
- C/C++ 的静态链接、.NET 的 single-file、Native AOT 都是有力选项
- 但对系统版本、架构、系统 DLL、安全模型的依赖不会消失
- Shell 扩展、服务、驱动、WebView2、部分 WinUI 3 场景,重点会落在系统注册和额外运行时上
- 单一可执行文件能否成功,取决于最初是否把「想合并的到底是什么」分清楚
如果一定要把单一可执行文件放在最优先,在技术选型阶段就朝着降低与系统的耦合度的方向去设计,会大幅提高成功率。
10. 参考资料
- 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
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
IE 模式之后换成 WebView2 就够了吗 ── ActiveX 无法运行的限制与现实的迁移设计
本文从内部系统的角度,梳理 WebView2 的基本结构、Evergreen 与 Fixed Version 两种分发策略、用户数据文件夹的陷阱、原生与 Web 的联动方式,并在「ActiveX 无法运行」这一限制的前提下,整理出摆脱 IE 模式依赖系统的现实迁移顺序。
委托开发 Windows 应用程序前该梳理的事项
在委托外包开发 Windows 应用程序之前,梳理现有软件改造、设备联动、COM/ActiveX、发布与更新、维护等需要注意的要点。
Windows 应用安全处理子进程的清单 - Job Object、退出传播、标准输入输出、watchdog 最佳实践
在 Windows 应用中安全处理子进程,比起选择启动 API,更重要的是设计进程树的所有权和结束流程。本文整理 Job Object、退出传播、标准输入输出与 watchdog。
如何理解 Windows 的会话隔离 ── Session 0・RDP・多用户同时运行
整理 Windows 应用程序开发者容易混淆的「会话」概念。说明服务无法显示 UI 的 Session 0 隔离原因、RDP 连接时会话的行为、命名对象的会话隔离,以及共享 PC・RDS 环境中常见的设计失误,从实务角度解说。
防止 Windows 应用程序重复启动 ── 命名 Mutex 与重复执行时的窗口前置
整理业务型 Windows 应用程序的常见需求——「不让同一个应用程序启动两次」——如何用命名 Mutex 来实现。涵盖 Global\ 与 Local\ 命名空间差异在 RDP 环境中的陷阱、拥有线程限制与 AbandonedMutexException、在 SetFor...
常见问题
汇总了咨询这一主题时常见的问题。
- Windows 应用可以只用一个 EXE 文件分发吗?
- 如果是单独启动的桌面工具,可以做到相当高的程度。使用 C/C++ 的静态链接、.NET 的 self-contained 或 single-file、Native AOT,都能把发布物合并成一个 EXE。但「能做成 1 个 EXE」和「不依赖目标 Windows」是两件不同的事,对系统版本、架构、系统 DLL、安全模型的依赖并不会消失。
- .NET 的 single-file 是不是就意味着不再依赖系统?
- 不是。single-file 主要减少的是应用发布物的集合数量,而不是对系统的依赖。framework-dependent 依赖目标环境已安装的 .NET,self-contained 会把 .NET 运行时也一并带上,Native AOT 能进一步减少启动期依赖,但会带来功能上的限制。single-file 和 Native AOT 基本上都与系统版本和架构相关,因此需要先确定最低支持的 Windows 版本和目标架构。
- 哪些应用类型很难做成单个 EXE?
- Shell 扩展、Windows 服务、驱动、基于 WebView2 的 UI、部分 WinUI 3 场景都比较困难。这些应用的重点不在文件数量,而在于需要向系统注册什么,或依赖哪些额外运行时:Shell 扩展需要向 Explorer 注册,服务需要向 SCM 注册并设计权限和启动账户,驱动需要 INF 文件和签名才能成立,WebView2 则需要先决定 WebView2 Runtime 的分发方式。
- 有没有比硬塞进单个 EXE 更好的分发方式?
- 把 DLL 放在 EXE 旁边的 app-local 分发是很有力的选择。哪怕是「app.exe + 少量相邻 DLL」这种形式,只要能做到无需安装程序、无需管理员权限、可以直接 xcopy 分发,往往比硬压缩成 1 个 EXE 更容易维护。关键是要先弄清楚:到底是想把发布物合并成一个,还是想省掉运行时预安装,还是想去掉安装程序,这三者需要分开考虑。
作者简介
本文作者的个人简介页面。
Go Komura
小村软件有限公司 代表
以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。