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.dll
  • user32.dll
  • advapi32.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-fileself-containedNative 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. 参考资料

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

常见问题

汇总了咨询这一主题时常见的问题。

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 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表