在 Windows 上,單一執行檔(single binary)能做到什麼程度 - 能收進 1 個 EXE 的範圍、無法消除的 Windows 依賴、以及發布前的判斷表

· · 發布, 單一執行檔, .NET, C++, WebView2, WinUI

這篇文章起源於下面這則貼文。

「如果可以,就想用 1 個檔案發布」——這在 Windows 上是再普通不過的需求。公司內部工具、設備整合工具、監控端、離線環境,或是盡量避免跑 installer 的現場,做成單一執行檔 都相當有吸引力。

不過這件事若沒有一開始就釐清,討論常常在中途就兜不攏。Windows 上講「想做成單一執行檔」時,裡面其實容易混進下面 4 種需求。

  • 發布物想只有 1 個
  • 希望不用事先安裝 .NET 或 Visual C++ 的 runtime
  • 希望不用 installer、不需系統管理員權限,放著就能跑
  • 希望不受「目標 Windows」版本或差異影響

這 4 種需求並不一樣。實務上比較不會偏的思考方式是:

把發布物收成 1 個 EXE 是相當可行的。
但要完全消除對「目標 Windows」的依賴,做不到。

本文就以 Windows 應用實務為角度,把這條界線整理清楚。

1. 先下結論

先直接把結論列出來:

  • 一般桌面 EXE,其實可以做到很高程度的 single binary
  • 能收成 1 個 EXE不依賴目標 Windows 是兩回事
  • Shell 擴充、Windows 服務、驅動、WebView2、WinUI 3 的部分情境,重點不在檔案數,而是 要向 OS 註冊什麼、要以什麼為前提
  • 實務上最重要的是分清楚:是要 single binary、還是想免 installer、還是想降低 OS 依賴

換句話說,在 Windows 上這條界線其實很實務:

  • 把發布物收成 1 個:相當可行
  • 把額外 runtime 吞進來:相當可行
  • 做到 xcopy 發布:視應用種類而定
  • 消除對目標 Windows 的依賴:不可能

2. 把「單一執行檔」分成 4 個層次

2.1 Level A:發布物就只有 1 個

最表面的一層:

  • 郵件用 1 個附件寄出
  • USB 放 1 個檔
  • 到目的地只放 app.exe

這只是 發布單位的外觀。實際上它可能在啟動時暫存解壓縮,也可能依賴 OS 側 DLL,但光就這個條件,是能達成的。

2.2 Level B:目標機器不需預先安裝語言 runtime

接下來是不需要事先裝 .NET runtime 或 VC++ 再發行套件:

  • C/C++ 靜態連結
  • .NET self-contained
  • .NET single-file
  • .NET Native AOT

到這一層,「可以單獨帶走」的感覺就相當明顯。

2.3 Level C:不用安裝或註冊

這一層會突然變難。

單純的 EXE 也許放著就能跑,但下面這些就不同:

  • Shell 擴充
  • Windows 服務
  • 自訂 URL scheme 或副檔名關聯
  • 驅動
  • 會被 Explorer、Office 等其他行程載入的元件

這塊光 放檔案 是不夠的,需要向 OS 註冊,或跟宿主端對接。

2.4 Level D:不依賴目標 Windows

在 Windows 上做不到。

Windows 應用最終仍然要跑在 Windows 的 API、loader、安全模型、裝置堆疊上。single binary 能處理到的範圍,只到 應用這側的責任範圍,並不包含把 OS 本身也帶走。

3. 比較容易收成 1 個 EXE 的領域

在 Windows 上,下列型態的應用比較容易收成 1 個 EXE。

  • 單獨啟動的桌面工具
  • EXE 本身就同時負責 UI 與處理的業務應用
  • 通訊、檔案處理、日誌收集、監控、設備控制這類工具
  • 不需要跟 Explorer、Office 做宿主整合的
  • 不以 Web runtime 為前提的 UI

這種應用,下列內容可以相當輕鬆地放進本體:

  • 自家程式碼
  • 資源
  • manifest
  • 預設設定
  • 範本資料
  • 部分第三方函式庫
  • 語言 runtime 本體

此外,就算沒有把 DLL 完全嵌入 EXE,把 DLL 放在 EXE 旁邊(app-local) 在 Windows 上還是相當實用。實務上,

  • 1 個 app.exe
  • 或是 app.exe + 幾個相鄰 DLL
  • 並且不需 installer、不需系統管理員權限、可直接 xcopy 發布

這種形式往往比硬要壓縮成 1 個 EXE 更容易維護。

4. 即使做成 1 個 EXE 也無法消除的 Windows 依賴

如果以為「只要 EXE 只有 1 個,就不依賴目標 Windows」,在這裡就會出事。實際上就算是 1 個 EXE,以下依賴仍在。

4.1 OS 版本依賴

Windows API 各自有最低支援版本,x64/Arm64 也有差異。就算打包成 1 個 EXE,還是需要先決定:

  • 要支援到 Windows 10 嗎
  • 是以 Windows 11 為前提嗎
  • Windows Server 上也要跑嗎
  • x86 / x64 / Arm64 哪一個是目標

4.2 系統 DLL 依賴

就算你覺得做成 1 個 EXE 了,實際執行時當然會用到 OS 提供的元件:

  • kernel32.dll
  • user32.dll
  • advapi32.dll
  • COM 基礎
  • 服務控制基礎

這些是 Windows 側的責任範圍。

4.3 安全模型依賴

  • UAC
  • 檔案 ACL
  • 服務控制管理員(SCM)
  • 登錄
  • 驅動簽章政策

這些,應用程式不可能獨自扛起來。

4.4 宿主與 runtime 依賴

若不是單獨啟動的 EXE,而是要跑在某個宿主裡的設計,依賴會一下子增加:

  • 用 WebView2:需要 WebView2 Runtime
  • 用 WinUI 3 / Windows App SDK:需要把發布模式釐清
  • 做 Shell 擴充:需要向 Explorer 註冊

也就是說,UI 或整合方式的選擇,直接決定了發布的困難度

5. 依技術別看現實的落地點

5.1 Native C/C++

Native C/C++ 是 single binary 自由度比較高的一側,有靜態連結空間,單獨啟動的 EXE 很容易收攏。

不過比起「全部壓進一個檔」,實務上更重要的是:

  • UCRT、VC++ runtime 怎麼處理
  • 第三方 DLL 要不要放在 app-local
  • 目標 CPU / OS 要收到多窄

5.2 .NET

.NET 有 single-fileself-containedNative AOT,表面上發布單位可以做得很小。

但要分清楚:

  • framework-dependent:仍依賴目標環境的 .NET
  • self-contained:把 .NET runtime 吞進去
  • single-file:把發布物集中成一個
  • Native AOT:進一步減少啟動期依賴,但有功能限制

「single-file 就不依賴 OS」這句話並不對。減少的主要是 應用發布物的集合

5.3 WebView2

採用 WebView2,single binary 的難度就會大不同。這時的主題不是 EXE 的數量,而是 WebView2 Runtime 怎麼處理

更正確的問法不是「能不能做成 1 個 EXE」,而是:

  • 是否以現場已有 Runtime 為前提
  • 是否使用 Evergreen
  • 是否同捆 Fixed Version
  • 離線部署要扛到哪裡

5.4 WinUI 3 / Windows App SDK

WinUI 3 也是一選下去,發布要求就跟著變。UI 技術的選擇,直接變成發布方式的選擇。

若 single binary 是第一優先,先回頭檢視 UI 技術的前提 往往比較快。

6. 本質上就需要「註冊、依賴」的領域

6.1 Shell 擴充

被 Explorer 載入的 Shell 擴充,跟「放著就能跑的 EXE」本質不同。這塊的主題不是幾個檔,而是 怎麼向 Explorer 註冊

6.2 Windows 服務

就算服務本身的 exe 可以收成 1 個檔,發布還是另一回事:

  • SCM 註冊
  • 權限
  • 啟動帳號
  • 復原設定

都得想。也就是說,服務這塊,重點在「怎麼安裝」,而不是「做成 1 個 EXE」。

6.3 驅動

驅動更是明確:INF、簽章、安裝流程都要備齊才成立,從一開始就不太站在 single binary 的擂台上。

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 個
  • 想省掉事先安裝 runtime
  • 想免 installer
  • 想讓離線更新更簡單

這個答案會決定要選什麼技術。

8.2 先固定最低支援的 Windows 與 arch

single-file 與 Native AOT 基本上都是 OS / architecture specific。若這一點含糊,就硬推「反正收成一個檔」,最後會被 API 缺失或 runtime 不匹配擋下。

8.3 把「同捆」與「交給 Windows」明文化

實務上光是把這張表寫下來就能減少很多意外:

  • 要同捆的
    • 本體 exe
    • 自製 DLL
    • 設定範本
    • self-contained runtime
  • 交給 Windows 的
    • 系統 DLL
    • OS API
    • SCM / 登錄 / Explorer
    • 驅動基礎
  • 另外以前提存在的
    • WebView2 Runtime
    • VC++ Redistributable
    • Office / Excel
    • 專用驅動

8.4 想優先 single binary,就減少宿主整合

這招相當有效。

  • 放棄 Shell 擴充,改回普通 EXE
  • 不做服務化,改用工作排程器或明示啟動
  • 不用 WebView2,改用原生 UI
  • COM 都收在自己的行程內

簡單說,越減少「讓 OS 載入」「向 OS 註冊」的設計,就越靠近 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 註冊與額外 runtime
  • single binary 成敗,取決於一開始就把「要把什麼收成 1 個」分清楚

若一定要優先 single binary,在技術選型階段就 朝著降低與 OS 的耦合度 去設計,成功率會高很多。

10. 參考資料

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽