更新紀錄(2 筆,最後更新 2026年09月04日)
本文的修改紀錄。已保存的更新前版本,可透過附有 DOI 的永久連結閱讀。
引用本文(DOI: 10.5281/zenodo.21616338)
本文保存於 Zenodo。以下同時提供一律指向最新版本的 DOI,以及固定於您正在閱讀版本的 DOI。
Go Komura(2026)。〈Windows 應用程式的單一檔案散發 - 單一執行檔與 OS 相依性的界線〉。小村軟體有限公司。https://doi.org/10.5281/zenodo.21616338 https://comcomponent.com/zh-TW/blog/2026/03/19/003-windows-single-binary-and-os-dependencies/
- DOI(最新版本)
- 10.5281/zenodo.21616338
- DOI(此版本)
- 10.5281/zenodo.22297177
這篇文章的起點,是一段「在 Windows 上,到哪裡為止還能稱為 單一執行檔」的閒聊。引發討論的貼文如下。就算跳過這一段,後面的內容也寫成可以單獨讀完的形式。
在 Windows 上,「可以的話想用 1 個檔案散發」是相當普通的需求。公司內部工具、設備介接工具、監控用電腦、離線環境,以及盡量不想用安裝程式的實務現場,做成單一執行檔 都很有吸引力。
不過這個題目如果不在一開始就釐清,討論到中途大多會兜不攏。因為在 Windows 上說「想做成單一執行檔」時,其實很容易混進 4 件不同的事。
- 想讓散發物只有 1 個
- 想讓 .NET 或 Visual C++ 的執行階段不必事先安裝
- 想不用安裝程式、不用系統管理員權限,放著就能跑
- 不想受目標 Windows 的差異影響
這 4 件事並不相同。以實務來說,這樣想最不會偏。
把散發物收攏成 1 個 EXE,相當程度做得到。 但要把對目標 Windows 的依賴降到零,做不到。
本文就以 Windows 應用程式的實務角度,把這條界線梳理清楚。
flowchart TB
accTitle: 就算收成 1 個 EXE 也消不掉 OS 相依性
accDescr: 想做成單一執行檔的需求裡容易混進散發的議題與相依性的議題,把散發物收攏成 1 個 EXE 相當程度做得到,但無法把對目標 Windows 的依賴降到零的圖。
a0["「想做成單一執行檔」"] --> a1["散發的議題:只要 1 個、放著就能跑"]
a0 --> a2["相依性的議題:不需執行階段、不依賴 OS"]
a1 --> a3["收攏成 1 個 EXE 相當可行"]
a2 --> a4["無法把 OS 相依性降到零"]
圖 1: 「想用 1 個檔案散發」裡,混著做得到的事與做不到的事。
1. 先講結論
先把結論整理出來,就是這樣。
- 一般的桌面 EXE,single binary 化可以做到相當高的程度
- 但是,能收成 1 個 EXE 與 不依賴目標 Windows 是兩回事
- Shell 擴充、Windows 服務、驅動程式、WebView2、WinUI 3 的一部分,比起檔案數量,要向 OS 註冊什麼、以什麼為前提 更容易成為真正的題目
- 實務上最重要的,是把 要做 single binary、要免安裝程式、還是要降低 OS 相依性 分開來決定
換句話說,在 Windows 上的界線是這樣劃的。
- 把散發物收攏成 1 個:相當可行
- 把額外的執行階段包進來:相當可行
- 做到 xcopy 散發:視應用程式種類而定
- 消除目標 Windows 這側的相依性:不可能
flowchart TB
accTitle: Windows 上的界線
accDescr: 把散發物收攏成 1 個與同捆額外的執行階段都相當可行,能不能做到 xcopy 散發要看應用程式種類,消除目標 Windows 這側的相依性則不可能的圖。
b1["把散發物收攏成 1 個"] --> b2["相當可行"]
b3["同捆額外的執行階段"] --> b2
b4["做到 xcopy 散發"] --> b5["視應用程式種類而定"]
b6["消除 OS 這側的相依性"] --> b7["不可能"]
圖 2: 做得到、視種類而定、做不到的界線。
1.1 本文使用的術語
先把後面會反覆出現的詞整理一下。
| 術語 | 全稱/說明 | 在本文中的意思 |
|---|---|---|
| UCRT | Universal C Runtime | Visual Studio 2015 把 C 執行階段拆分之後留下的標準 C 函式庫部分。Windows 10 以後已經當成 OS 的組成元件一起附帶 |
| VC++ 可轉散發套件 | Visual C++ Redistributable | 把 UCRT 以外的 Visual C++ 執行階段 DLL 裝進目標電腦的安裝程式 |
| framework-dependent | 依賴目標環境 .NET 的散發 | 以目標電腦已經裝好 .NET 執行階段為前提的散發形態 |
| self-contained | 同捆 .NET 執行階段 | 應用程式這側帶著一整套 .NET 執行階段一起散發的形態 |
| single-file | 單一檔案發行 | 把散發物彙整成 1 個 EXE 的 .NET 發行選項 |
| Native AOT | Native Ahead-Of-Time | 在發行時就把 IL 事先編譯成原生程式碼的散發形態。程式跑起來之後不使用 JIT |
| app-local 散發 | 應用程式相鄰散發 | 把 DLL 放在與 EXE 相同的資料夾,只給該應用程式使用的散發方式 |
| xcopy 散發 | 只要複製就好的散發 | 不用安裝程式也不用系統管理員權限,整個資料夾放過去就能跑的散發方式 |
| SCM | Service Control Manager,服務控制管理員 | 管理 Windows 服務註冊、啟動、停止的 OS 機制 |
| UAC | User Account Control,使用者帳戶控制 | 控制提升到系統管理員權限的 Windows 安全機制 |
| Shell 擴充 | shell extension | 被載入檔案總管等處理程序內執行的 COM 元件 |
| Evergreen | 持續更新模式 | 把 WebView2 Runtime 交給自動更新的共用單一份的散發模式 |
| Fixed Version | 版本固定模式 | 把特定版本的 WebView2 Runtime 同捆進自己應用程式的散發模式 |
| arch | architecture,CPU 架構 | 指 x86 / x64 / Arm64 |
圖中實線表示始終成立的關係,虛線表示附帶條件的關係(成立條件寫在詳細頁面中各關係的說明中)。關係的完整清單(共 22 條,附依據與可信度)以及主要概念的定義,彙整在知識地圖詳細頁面(日文)。資料:JSON-LD / Turtle
2. 把「單一執行檔」拆成 4 個階段來想
先把階段畫成圖,就是下面這樣。從下往上依序變難,最上面那一層在 Windows 上到不了。
flowchart BT
A["層級 A:散發物只有 1 個<br/>相當可行"]
B["層級 B:不必事先安裝語言執行階段<br/>相當可行"]
C["層級 C:不需安裝或註冊<br/>視應用程式種類而定"]
D["層級 D:不依賴目標 Windows<br/>在 Windows 上到不了"]
A --> B
B --> C
C --> D
圖 3: 單一執行檔的 4 個階段。從下往上依序變難,層級 D 到不了。
「想做成單一執行檔」這句話,指的是這 4 層裡的哪一層,需要做的事完全不同。
2.1 層級 A:散發物只有 1 個
最表面的一層就是這個。
- 用郵件寄 1 個附件就好
- USB 裡放 1 個檔案就好
- 部署的位置只放一個
app.exe
這談的是 散發單位看起來的樣子。實際上就算啟動時會先暫時解壓縮,就算依賴 OS 這側的 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 等其他處理程序載入的元件
這個領域光是 把檔案放上去 是不夠的,還需要向 OS 註冊,或是跟宿主端接上線。
flowchart TB
accTitle: 光把檔案放上去不夠的領域
accDescr: Shell 擴充、Windows 服務、檔案關聯、驅動程式,以及會被其他處理程序載入的元件,光把檔案放上去並不夠,還需要向 OS 註冊或跟宿主端接上線的圖。
c1["Shell 擴充、服務"] --> c4["光把檔案放上去不夠"]
c2["檔案關聯、驅動程式"] --> c4
c3["會被其他處理程序載入的東西"] --> c4
c4 --> c5["需要向 OS 註冊、跟宿主接上線"]
圖 4: 層級 C 之所以突然變難,是因為進到了需要向 OS 註冊的領域。
2.4 層級 D:不依賴目標 Windows
在 Windows 上做不到。
因為 Windows 應用程式最後都跑在 Windows 的 API、載入器、安全模型、裝置堆疊之上。能做到 single binary 化的,只到應用程式這側的職責範圍,並不是連 OS 本身都一起帶走。
3. 相當容易收成 1 個 EXE 的領域
在 Windows 上,也有比較容易收攏成 1 個 EXE 的應用程式。
- 單獨啟動的桌面工具
- EXE 本身同時具備 UI 與處理邏輯的業務應用程式
- 通訊、檔案處理、日誌收集、監控、裝置控制這類工具
- 不需要與 Explorer 或 Office 做宿主整合的
- 不以 Web 執行階段為前提的 UI
這一類應用程式,能放進本體的東西很多。
- 自己寫的程式碼
- 資源
- 資訊清單(manifest)
- 預設設定
- 範本資料
- 一部分第三方函式庫
- 語言執行階段本體
而且,就算沒有把 DLL 完全嵌進 EXE 裡,把 DLL 放在 EXE 旁邊的 app-local 散發 在 Windows 上本來就很有力。實務上,
- 只有 1 個
app.exe - 或是
app.exe加上幾個相鄰的 DLL - 但不需要安裝程式、不需要系統管理員權限,用 xcopy 就能散發
這種形式比硬壓成 1 個 EXE 更容易維護的情況並不少見。
flowchart TB
accTitle: app-local 散發這個折衷點
accDescr: 就算沒有把 DLL 完全嵌進 EXE,只要把 DLL 放在 EXE 旁邊,不需安裝程式與系統管理員權限就能用 xcopy 散發,這種形式比硬壓成 1 個 EXE 更容易維護的情況並不少見的圖。
d1["app.exe 加上幾個相鄰 DLL"] --> d2["不需安裝程式、不需系統管理員權限"]
d2 --> d3["用 xcopy 就能散發"]
d3 -.-> d4["有時比硬壓成 1 個 EXE 更容易維護"]
圖 5: 不必執著於 1 個 EXE,用 app-local 散發往往就能達成目的。
4. 就算收成 1 個 EXE 也消不掉的 Windows 相依性
要是以為「只要 EXE 只有 1 個,就不依賴目標 Windows」,就會在這裡出事。實際上,就算收成 1 個 EXE,還是有相依性會留下來。
4.1 OS 版本相依性
Windows API 各自都有最低支援的 OS,x64 與 Arm64 之間也有差異。也就是說,就算做成單一 EXE,下列項目
- 要支援到 Windows 10 嗎
- 是以 Windows 11 為前提嗎
- Windows Server 上也要跑嗎
- x86 / x64 / Arm64 要以哪一個為目標
都必須在一開始就固定下來。
4.2 系統 DLL 相依性
就算自己覺得已經收成 1 個 EXE,程式跑起來當然還是會用到 OS 提供的元件。
kernel32.dlluser32.dlladvapi32.dll- COM 基礎架構
- 服務控制基礎架構
這一塊是 Windows 這側的職責範圍。
4.3 安全模型相依性
- UAC
- 檔案 ACL
- 服務控制管理員
- 登錄檔
- 驅動程式簽署原則
這些東西,應用程式沒辦法自己一個人扛下來。
flowchart TB
accTitle: 收成 1 個 EXE 之後仍留下的相依性
accDescr: 就算收成 1 個 EXE,Windows API 的最低支援 OS 與架構、kernel32.dll 等系統 DLL 與 COM 基礎架構,以及 UAC、ACL、驅動程式簽署原則這些安全模型上的相依性都會留下來的圖。
e0["1 個 EXE 的應用程式"] --> e1["OS 版本與 arch"]
e0 --> e2["系統 DLL、COM 基礎架構"]
e0 --> e3["安全模型"]
e1 --> e4["這些都不是應用程式扛得下來的"]
e2 --> e4
e3 --> e4
圖 6: 就算把散發物收成 1 個,對 Windows 職責範圍的依賴仍然存在。
4.4 宿主與執行階段相依性
如果不是單獨啟動的 EXE,而是要掛在某個宿主上的設計,相依性會一口氣增加。
- 使用 WebView2:需要 WebView2 Runtime
- 使用 WinUI 3 / Windows App SDK:需要先把散發模式梳理清楚
- 做 Shell 擴充:需要向 Explorer 這側註冊
也就是說,UI 與整合方式的選擇,往往直接變成散發的難度。
flowchart TB
accTitle: UI 與整合的選擇直接變成散發難度
accDescr: 使用 WebView2 就需要 WebView2 Runtime,使用 WinUI 3 就需要梳理散發模式,做 Shell 擴充就需要向 Explorer 這側註冊,宿主與執行階段的選擇直接變成散發難度的圖。
f1["使用 WebView2"] --> f2["需要 Runtime"]
f3["使用 WinUI 3"] --> f4["需要梳理散發模式"]
f5["做 Shell 擴充"] --> f6["需要向 Explorer 註冊"]
圖 7: 離單獨啟動的 EXE 越遠,相依性增加得越快。
5. 依技術別看現實的落地點
5.1 原生 C/C++
原生 C/C++ 在 single binary 化上的自由度屬於比較高的一側。有選擇靜態連結的空間,單獨啟動的 EXE 相當容易收攏。
不過,比起把全部塞進 1 個檔案,
- UCRT 與 VC++ 執行階段要怎麼處理
- 第三方 DLL 要不要放成 app-local
- 目標 CPU / OS 要收窄到什麼程度
這些在實務上更重要。
入門:/MT 與 /MD 的差別
在 MSVC 裡,決定「要不要把執行階段包進來」的,實際上就是這個編譯器選項。
| 選項 | 連結進來的東西 | 目標電腦上需要的東西 |
|---|---|---|
/MD |
ucrt.lib 與 vcruntime.lib 這兩個 DLL 用的匯入程式庫。程式跑起來時會用 ucrtbase.dll 與 vcruntime<版本>.dll |
UCRT 與 VC++ 執行階段 |
/MT |
靜態連結 libucrt.lib、libvcruntime.lib、libcmt.lib |
不需要額外的執行階段 |
最小的試法,在 Developer Command Prompt 裡只要這一行。
cl /std:c++17 /EHsc /O2 /MT app.cpp /Fe:app.exe
這裡有 3 點要注意。
- UCRT 在 Visual Studio 2015 把 C 執行階段拆分時成為 Windows 的組成元件,Windows 10 以後已經當成 OS 的一部分一起附帶。如果要以更早的 Windows 為目標,就需要用
vcredist轉散發。 - 不建議用
/MT建置 DLL 這一側。 靜態連結的 CRT 會把狀態封在那個 DLL 內部,所以 EXE 與 DLL 之間的記憶體配置、地區設定、_set_se_translator的作用範圍都會對不上。隨手把「EXE 用/MT,同捆的 DLL 也用/MT」湊成一致,就會在跨越邊界的配置與釋放上出事。 /clr與/MT不能併用。只要混進 C++/CLI,就得走/MD這一側。
flowchart TB
accTitle: 靜態連結的 3 個注意事項
accDescr: UCRT 在 Windows 10 以後由 OS 附帶但更早的版本需要用 vcredist 轉散發,用 /MT 建置 DLL 會把 CRT 的狀態封在 DLL 內而在跨邊界的配置與釋放上出事,以及 /clr 與 /MT 不能併用的圖。
g0["用 /MT 靜態連結"] --> g1["舊版 Windows 需要 vcredist"]
g0 --> g2["不建議在 DLL 這側用 /MT"]
g0 --> g3["不能與 /clr 併用"]
g2 -.-> g4["跨邊界的配置與釋放會出事"]
圖 8: /MT 很強大,但要注意 UCRT 的前提、DLL 邊界、C++/CLI 這 3 點。
5.2 .NET
.NET 有 single-file、self-contained、Native AOT,所以散發單位看起來可以做得相當小。
但還是要分清楚。
- framework-dependent:依賴目標環境的 .NET
- self-contained:把 .NET 執行階段包進來
- single-file:把散發物收攏成 1 個
- Native AOT:可以進一步減少啟動時的相依性,但也有功能上的限制
並不是「因為用了 single-file,OS 相依性就會減少」。減少的主要是 應用程式散發物的零散程度。
flowchart TB
accTitle: single-file 減少的是散發物的零散程度
accDescr: framework-dependent 依賴目標環境的 .NET,self-contained 把執行階段包進來,single-file 只是把散發物收攏成 1 個,並不是用了 single-file 就會減少 OS 相依性的圖。
h1["framework-dependent"] --> h2["依賴目標環境的 .NET"]
h3["self-contained"] --> h4["把 .NET 執行階段包進來"]
h5["single-file"] --> h6["把散發物收攏成 1 個"]
h6 -.-> h7["並不會因此減少 OS 相依性"]
圖 9: .NET 的每一種發行形態,減少的東西與留下的東西都不一樣。
入門:dotnet publish 的 3 種模式
想先跑起來看差別,這 3 行就夠了。
rem 1. framework-dependent + single-file: 目標電腦上需要 .NET 執行階段
dotnet publish -c Release -r win-x64 --self-contained false -p:PublishSingleFile=true
rem 2. self-contained + single-file: 把 .NET 執行階段包進來
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true
rem 3. Native AOT: 在 csproj 寫上 PublishAot 之後再發行
dotnet publish -c Release -r win-x64
PublishSingleFile 與 PublishAot,官方建議寫在 csproj 裡而不是寫在命令列。因為這樣建置過程中的相容性分析才會啟用,可以減少發行之後才發現的事故。
<PropertyGroup>
<PublishSingleFile>true</PublishSingleFile>
<PublishAot>true</PublishAot>
</PropertyGroup>
另外,只要指定 RuntimeIdentifier,SelfContained 預設就會變成 true。想要 framework-dependent 時,請明確加上 --self-contained false。要在 Windows 上發行 Native AOT,需要 Visual Studio 的 「使用 C++ 的桌面開發」 工作負載。
取捨:「收成 1 個」不是免費的
這一段如果只寫「做得到」會出事,所以也把代價一起列出來。
single-file 的啟動時解壓縮
- 預設只會把 managed DLL 打包進去。啟動時載入到記憶體上,不會解壓縮到資料夾裡。執行階段本體的原生二進位檔仍然是分開的檔案。
- 想連原生二進位檔都收進 1 個檔案,就用
IncludeNativeLibrariesForSelfExtract;想在執行前把全部都解壓縮出來,就用IncludeAllContentForSelfExtract。兩者都會變成「先解壓縮再啟動」的行為,在 Windows 上會解壓縮到%TEMP%\.net底下。位置可以用DOTNET_BUNDLE_EXTRACT_BASE_DIR更改,但不要指定成權限不同的使用者或服務也寫得進去的位置。 - 啟用
EnableCompressionInSingleFile之後 EXE 會小很多,但啟動時要在記憶體上解壓縮,啟動就會變慢。官方文件也寫成「使用之前先把大小與啟動成本兩邊都量過」。影響會因應用程式而有很大差異。
single-file 的 API 不相容
把散發物收成 1 個之後,以檔案路徑為前提的程式碼會悄悄壞掉。
| API | 在 single-file 下的行為 |
|---|---|
Assembly.Location |
傳回空字串 |
Assembly.CodeBase |
PlatformNotSupportedException |
Assembly.GetFile |
IOException |
Module.Name |
傳回 <Unknown> 這個字串 |
要碰 EXE 旁邊的檔案就改用 AppContext.BaseDirectory,需要執行檔的路徑就改用 Environment.ProcessPath。第三方函式庫內部也可能用到這些 API,所以做成 single-file 之後,一定要在實機上啟動確認一次。
flowchart TB
accTitle: 以路徑為前提的程式碼會悄悄壞掉
accDescr: 把散發物收成 1 個之後 Assembly.Location 會傳回空字串,以檔案路徑為前提的 API 行為都會改變,所以 EXE 旁邊的檔案改用 AppContext.BaseDirectory,執行檔的路徑改用 Environment.ProcessPath,並且一定要在實機上啟動確認一次的圖。
i1["做成 single-file"] --> i2["以路徑為前提的 API 行為改變"]
i2 --> i3["旁邊的檔案用 AppContext.BaseDirectory"]
i2 --> i4["執行檔用 Environment.ProcessPath"]
i3 --> i5["在實機上啟動確認一次"]
i4 --> i5
圖 10: 做成 single-file 之後,把取得路徑的方式換掉,並在實機上確認。
Native AOT 的功能限制
換來「啟動快、不需執行階段」的代價,是下面這些都不能用了。
- 像
Assembly.LoadFile這種 動態載入 - 像
System.Reflection.Emit這種 執行時產生程式碼 - C++/CLI
- 在 Windows 上是 內建 COM
- 因為必須做修剪(trimming),修剪的限制也會原封不動地跟著上來
- 實質上就是 single-file,所以上面的 API 不相容也會原封不動地跟著上來
System.Linq.Expressions一律會變成直譯執行,比執行時產生的程式碼慢
目標平台也是寫死的。.NET 8 在 Windows 上是 x64 與 Arm64,.NET 9 以後才加上 x86。「用 AnyCPU 做成 1 個」這種想法從一開始就不成立。
flowchart TB
accTitle: Native AOT 的代價
accDescr: Native AOT 可以減少啟動時的相依性,代價是不能用動態載入與執行時產生程式碼、不能用 C++/CLI 與 Windows 的內建 COM,修剪與 single-file 的限制也會跟著上來,目標平台同樣是寫死的圖。
j0["Native AOT"] --> j1["可以減少啟動時的相依性"]
j0 --> j2["不能動態載入、不能執行時產生程式碼"]
j0 --> j3["不能用 C++/CLI、內建 COM"]
j2 -.-> j4["修剪與 single-file 的限制也跟著上來"]
j3 -.-> j5["目標 OS 與 arch 都寫死"]
圖 11: Native AOT 的好處,要跟不能再用的功能一起看。
5.3 WebView2
採用 WebView2 之後,single binary 的難度會完全變樣。這裡真正的題目不是 EXE 的數量,而是 WebView2 Runtime 要怎麼處理。
在「能不能收成 1 個 EXE」之前,有幾個更該先想的問題。
- 是否以現場既有的 Runtime 為前提
- 要不要用 Evergreen
- 要不要同捆 Fixed Version
- 離線散發要負責到什麼程度
Evergreen 與 Fixed Version 的差別,大致是這樣。
| Evergreen | Fixed Version | |
|---|---|---|
| 負責更新的是 | Microsoft,會自動更新 | 自己,配合應用程式的更新一起換掉 |
| 電腦上的實體 | 所有應用程式共用 1 份 | 每個應用程式各自同捆 |
| 散發大小 | 幾乎不會增加 | 超過 250 MB |
| 安裝方式 | Bootstrapper 約 2 MB,需要多少就下載多少。離線時要同捆 Standalone Installer | 把解壓縮後的整套二進位檔跟應用程式一起散發 |
| 既有的限制 | 必須在啟動前檢查電腦上是否已經裝好 | 無法從網路路徑或 UNC 路徑執行 |
Evergreen Runtime 在 Windows 11 上是當成 OS 的一部分預先安裝好的。不過 Windows 10 這側還是有沒裝的電腦,所以 Microsoft 自己也寫成「就算選 Evergreen,Runtime 還是散發出去比較好」。也就是說,由應用程式這側檢查有沒有,不夠就裝上去 的步驟,不管選哪一種都需要。Fixed Version 換來的是「更新時機可以自己掌控」,代價是散發物會增加數百 MB。這件事跟 1 個 EXE 的討論是分開的,必須先決定。
flowchart TB
accTitle: WebView2 要先決定散發方式
accDescr: Evergreen 是由 Microsoft 自動更新的共用單一份但電腦上也可能沒裝,Fixed Version 換來自己掌控更新但散發物會增加數百 MB,所以不論選哪一種都需要由應用程式這側檢查有沒有、不夠就裝上去的步驟的圖。
k0["採用 WebView2"] --> k1["Evergreen:共用的 1 份自動更新"]
k0 --> k2["Fixed Version:自己同捆與更新"]
k1 --> k3["檢查有沒有,不夠就裝上去"]
k2 -.-> k4["散發物增加數百 MB"]
圖 12: WebView2 真正的題目不是 EXE 的數量,而是 Runtime 的處理方式。
5.4 WinUI 3 / Windows App SDK
WinUI 3 也一樣,採用的當下散發需求就跟著變。UI 技術的選擇,直接就是散發方式的選擇。
具體來說,要決定的有 2 個面向。
- packaging:要 packaged(MSIX)、指向外部位置的 packaged,還是 unpackaged
- runtime:要 framework-dependent 還是 self-contained
而從 single binary 的角度看,真正會影響結果的是下面這些組合上的限制。
- 能用
PublishSingleFile的,只有 unpackaged 而且 self-contained 的 WinUI 3 應用程式,而且需要 Windows App SDK 1.5 以後的版本。 - packaged 的應用程式,以及指向外部位置的 packaged 應用程式,都不能用
PublishSingleFile。 - 改成 unpackaged 之後會失去 package identity,所以通知、背景工作、檔案關聯、快顯功能表擴充這些以 package identity 為前提的 Windows 功能就都不能用了。
也就是說,在 WinUI 3 上,「收成 1 個 EXE」與「使用 Windows 的擴充功能」是 正面衝突的。如果把 single binary 放在最優先,先回頭重新檢視 UI 技術的前提 往往比較快。
flowchart TB
accTitle: 1 個 EXE 與 package identity 的衝突
accDescr: WinUI 3 能用 PublishSingleFile 的只有 unpackaged 而且 self-contained 的組態,改成 unpackaged 就會失去 package identity 而不能用通知與檔案關聯等功能,所以 1 個 EXE 與 Windows 的擴充功能正面衝突的圖。
m1["想使用 PublishSingleFile"] --> m2["只有 unpackaged+self-contained"]
m2 --> m3["失去 package identity"]
m3 --> m4["不能用通知、檔案關聯等功能"]
m4 -.-> m5["1 個 EXE 與擴充功能正面衝突"]
圖 13: 在 WinUI 3 上,選擇 1 個 EXE 就等於放棄 package identity。
6. 本質上就需要「註冊與相依」的領域
6.1 Shell 擴充
會被 Explorer 載入的 Shell 擴充,跟單純的「放著就能跑的 EXE」是兩種東西。這裡真正的題目不是檔案數量,而是 要怎麼向 Explorer 註冊。
6.2 Windows 服務
就算服務本體的 exe 可以做成 1 個檔案,散發還是另一個問題。
- 向 SCM 註冊
- 權限
- 啟動帳戶
- 修復設定
這些都必須先想清楚。也就是說,服務屬於比起「收成 1 個 EXE」,更該把「要怎麼安裝」敲定細節的領域。
6.3 驅動程式
驅動程式更明確。要連 INF、簽署、安裝步驟都齊備才算成立,所以從一開始就很難站上 single binary 的擂台。
flowchart TB
accTitle: 註冊與簽署才是真正題目的領域
accDescr: Shell 擴充要向 Explorer 註冊,Windows 服務要向 SCM 註冊並設計權限與啟動帳戶,驅動程式要連 INF 與簽署都齊備才成立,所以比起檔案數量,註冊與相依的設計才是真正的題目的圖。
n1["Shell 擴充"] --> n2["向 Explorer 註冊"]
n3["服務"] --> n4["SCM 註冊、權限、帳戶"]
n5["驅動程式"] --> n6["INF 與簽署"]
n4 -.-> n7["比起檔案數量,註冊的設計才是題目"]
圖 14: 這 3 個領域要敲定的不是「收成 1 個 EXE」,而是「要怎麼註冊」。
7. 實務上的判斷表
想粗略判斷的話,這張表很好用。
| 想做的東西 | 收成 1 個 EXE 的現實度 | 該先想的事 |
|---|---|---|
| 單獨啟動的 Win32 / C++ 工具 | 高 | 靜態連結、目標 OS / arch |
| 單獨啟動的 WinForms / WPF 工具 | 高 | self-contained、single-file、Native AOT 的適用性 |
| WinUI 3 / Windows App SDK 應用程式 | 中 | 散發模式、額外的相依性 |
| 以 WebView2 為基礎的桌面 UI | 低到中 | Runtime 的散發方式 |
| Explorer 右鍵擴充或預覽 | 低 | COM/登錄檔註冊 |
| Windows 服務 | 中 | SCM 註冊、權限、更新步驟 |
| 同捆驅動程式的應用程式 | 低 | INF、簽署、安裝 |
這張表最重要的一點,是讓人看清 「二進位檔的數量」與「散發的職責範圍」是兩回事。
8. 散發設計上該先決定的事
想讓 single binary 化成功,有些事最好在實作之前就先決定。
8.1 決定想把什麼收成 1 個
- 是想把散發物收成 1 個
- 是想省掉事先安裝執行階段
- 是想不用安裝程式
- 還是想讓離線更新更簡單
答案不同,要選的技術也不同。
8.2 一開始就固定最低支援的 Windows 與 arch
single-file 與 Native AOT 基本上都是 OS / architecture specific。這一點含糊不清就一路推「總之做成 1 個檔案」,最後會被 API 不足或 runtime 不一致卡住。
8.3 把「要同捆的」與「交給 Windows 的」白紙黑字寫下來
實務上,光是把這張表寫下來,事故就會少很多。
- 要同捆進應用程式的
- 本體 exe
- 自製 DLL
- 設定範本
- self-contained runtime
- 交給 Windows 的
- 系統 DLL
- OS API
- SCM/登錄檔/Explorer
- 驅動程式基礎架構
- 另外以前提存在的
- WebView2 Runtime
- VC++ Redistributable
- Office / Excel
- 專用驅動程式
flowchart TB
accTitle: 把職責範圍的 3 種分類白紙黑字寫下來
accDescr: 光是把要同捆進應用程式的、交給 Windows 的、另外以前提存在的這 3 種分類寫下來,散發時的事故就會少很多的圖。
p0["散發物的職責範圍"] --> p1["要同捆進應用程式的"]
p0 --> p2["交給 Windows 的"]
p0 --> p3["另外以前提存在的"]
p3 -.-> p4["光是寫下來事故就會減少"]
圖 15: 同捆、交給 OS、另外以前提存在的 3 種分類,要先白紙黑字寫下來。
8.4 想優先做到 single binary,就減少宿主整合
這一招相當管用。
- 放棄 Shell 擴充,改回普通的 EXE
- 不做成服務,改用工作排程器或明確啟動就好
- 不用 WebView2,改用原生 UI
- COM 收在自己的處理程序內
說到底,越是減少讓 OS「載入」「註冊」的設計,就越靠近 single binary。
flowchart TB
accTitle: 越減少宿主整合就越靠近
accDescr: 放棄 Shell 擴充改回普通 EXE,不做成服務而改用工作排程器或明確啟動,不用 WebView2 而改用原生 UI,越是減少讓 OS 載入與註冊的設計就越靠近 single binary 的圖。
q1["放棄 Shell 擴充改回普通 EXE"] --> q4["減少向 OS 註冊的設計"]
q2["不做成服務改為明確啟動"] --> q4
q3["不用 WebView2 改用原生 UI"] --> q4
q4 --> q5["靠近 single binary"]
圖 16: 想優先做到 single binary,就要減少宿主整合本身。
9. 總結
在 Windows 上做 single binary 化,可以做到相當高的程度。不過最後歸結起來就是這一句。
把應用程式收成 1 個 EXE,做得到。 但要連那個應用程式所依賴的 Windows 也收成 1 個 EXE,做不到。
特別想記住的有 5 點。
- 單獨啟動的一般 EXE,可以相當接近 1 個檔案的散發
- C/C++ 的靜態連結、.NET single-file、Native AOT 都很有力
- 但 OS 版本、arch、系統 DLL、安全模型上的相依性不會消失
- Shell 擴充、服務、驅動程式、WebView2、WinUI 3 的一部分,主角會變成 OS 註冊與額外執行階段的問題
- single binary 的成敗,取決於一開始有沒有把「想把什麼收成 1 個」釐清
如果要把 single binary 放在很優先的位置,在技術選型階段就朝著 降低與 OS 的耦合度 去設計,成功率會高很多。
flowchart TB
accTitle: 成功的關鍵在與 OS 的耦合度
accDescr: 就算能把應用程式收成 1 個 EXE,也無法連它所依賴的 Windows 一起收成 1 個 EXE,所以要把 single binary 放在很優先的位置時,在技術選型階段就朝著降低與 OS 的耦合度去設計會比較容易成功的圖。
r1["把應用程式收成 1 個 EXE"] --> r2["做得到"]
r3["連依賴的 Windows 也收成 1 個 EXE"] --> r4["做不到"]
r4 -.-> r5["所以在技術選型時降低與 OS 的耦合度"]
圖 17: 1 個 EXE 化的成敗,取決於一開始與 OS 的耦合度設計。
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
- Microsoft Learn, Package and deploy Windows apps overview
- Microsoft Learn, Packaging overview - Windows apps
- Microsoft Learn, Evergreen vs. fixed version of the WebView2 Runtime
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
Time Travel Debugging ── 把長期運轉中無法重現的問題「錄下來」再倒回去
一個月才出現一次的問題,當機傾印只拍得到結果。本文說明如何用 WinDbg 的 Time Travel Debugging(TTD) 錄下執行並倒回,涵蓋 TTD.exe 的錄製設計、環形緩衝區、TTD.Calls 查詢,以及和傾印的分工。
引數為什麼會壞掉 ── Windows 命令列引數的規則
在 Windows 上並不存在引數的陣列,傳給 CreateProcess 的是一條字串,分割由接收端負責。本文說明 CommandLineToArgvW・CRT・.NET 的分割規則,以及 .NET 的 ArgumentList 與 C++ 中正確的組裝方式。
IE 模式之後改用 WebView2 就好嗎 ── ActiveX 無法運作的限制與現實的遷移設計
本文從內部系統的角度,整理 WebView2 的基本結構、Evergreen 與 Fixed Version 兩種配布策略、使用者資料資料夾的陷阱、原生與 Web 的連結方式,並在「ActiveX 無法運作」這一限制的前提下,梳理出脫離 IE 模式依賴系統的現實遷移順序。
委外・委託開發 Windows 應用程式前該整理的事項
在委外・委託開發 Windows 應用程式之前,整理既有軟體改版、設備整合、COM/ActiveX、發布與更新、維護等應留意的重點。
Windows 應用程式安全處理子處理程序的檢查清單
在 Windows 應用程式中安全處理子處理程序,比起挑選啟動 API,更重要的是處理程序樹的所有權與結束流程的設計。本文梳理 Job Object、結束傳播、標準輸入輸出與 watchdog。
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
Windows 應用程式的散發,把 single-file 化、執行階段同捆、要不要採用 WebView2 或 WinUI、該不該做成服務都一起納入設計,比較能減少回頭重做。
技術諮詢 & 設計審查
「想收成 1 個 EXE」這個需求,只要把散發單位、OS 相依性、是否需要註冊、更新責任分開梳理,就比較好判斷。
常見問題
整理諮詢這個主題時常見的問題。
- Windows 應用程式可以只用 1 個 EXE 檔案散發嗎?
- 如果是單獨啟動的桌面工具,可以做到相當高的程度。用 C/C++ 的靜態連結、.NET 的 self-contained 或 single-file、Native AOT,都能把散發物收攏成 1 個 EXE。但是「能收成 1 個 EXE」與「不依賴目標 Windows」是兩回事,對 OS 版本、CPU 架構、系統 DLL、安全模型的相依性並不會消失。
- 改用 .NET 的 single-file 之後,OS 相依性就會消失嗎?
- 不會。single-file 減少的主要是應用程式散發物的零散程度,並不是減少 OS 相依性。framework-dependent 依賴目標環境的 .NET,self-contained 把 .NET 執行階段包進來,Native AOT 可以進一步減少啟動時的相依性,但也有功能上的限制。single-file 與 Native AOT 基本上都是 OS 與架構專屬的,所以最低支援的 Windows 與 arch 必須在一開始就固定下來。
- 哪些應用程式比較難收成 1 個 EXE?
- Shell 擴充、Windows 服務、驅動程式、以 WebView2 為基礎的 UI,以及 WinUI 3 的一部分。這些真正的題目不是檔案數量,而是要向 OS 註冊什麼、要處理哪些額外的執行階段。例如 Shell 擴充必須向 Explorer 註冊,服務需要向 SCM 註冊並設計權限與啟動帳戶,驅動程式要連 INF 與簽署都齊備才成立,所以放著就能跑的散發方式並不成立。WebView2 則必須先決定 WebView2 Runtime 的散發方式。
- 比起硬把東西塞進 1 個 EXE,有沒有更好的散發方式?
- 把 DLL 放在 EXE 旁邊的 app-local 散發是很有力的選項。就算形式是 app.exe 加上幾個相鄰的 DLL,只要能不用安裝程式、不用系統管理員權限就以 xcopy 散發,往往比硬壓成 1 個 EXE 更容易維護。重點是在一開始就分清楚:是想讓散發物只有 1 個,還是想省掉事先安裝執行階段,還是想不用安裝程式。