.NET5.0 單檔案發布打包操作深度剖析
前言
隨著 .NET5.0 Preview 8 的發布,許多新功能正在被社區成員一一探索;這其中就包含了“單檔案發布”這個炫酷的功能,實際上,這也是社區一直以來的呼聲,從 WinForm 的 msi 開始,我們就希望有這樣一個功能,雖然在 docker 時代,單檔案發布的功能顯得“不那么重要”,但正是從這一點可以看出,.NET 的團隊成員一直在致力于實用功能的完善,
在 Java 的世界里,單檔案發布一直伴隨著他們的成長,War 檔案可以直接上傳到 Tomcat 上運行,話說我們還是有那么一丟丟的羨慕的,不過凡事有利就有弊,單檔案發布對于細分模塊的熱更新來說,還有有一點點的不方便,
不過瑕不掩瑜,在微服務概念越來越火熱的今天,相信單檔案發布的功能帶給大家更多的是興奮,
什么是單檔案發布
首先,我們要清楚的了解,什么是單檔案發布,
官方的目標定義:
.Net 5.0單個檔案解決方案應為:
- 廣泛兼容:可以將包含IL程式集,隨時運行的程式集,復合程式集,本機二進制檔案,組態檔等的應用程式打包為一個可執行檔案,
- 可以直接從打包軟體直接運行應用程式的托管組件,而無需提取到磁盤,
- 可與除錯器和工具一起使用,
從上面的目標可以看出,和以往版本最大的不同在于:將所有依賴打包到一個可執行檔案中,可直接運行,不影響除錯操作,
注意上面的這句話“將所有依賴打包到一個可執行檔案中”,而在以往,我們使用 dotnet publish 將應用程式進行發布之后,我們會看到,在 publish 下有許多專案依賴的 dll 檔案,在 .NET5.0 到來之后,這些依賴檔案可收納到一個檔案中,瞬間讓人感受到了清涼,
發布操作指令相關
命令
| 平臺 | 命令 | 說明 |
|---|---|---|
| Linux | dotnet publish -r linux-x64 /p:PublishSingleFile=true | - |
| Windows | dotnet publish -r win-x64 --self-contained=false /p:PublishSingleFile=true | - |
| Mac OS | - | - |
可選引數
| 屬性 | 描述 |
|---|---|
| IncludeNativeLibrariesInSingleFile | 在發布時,將依賴的本機二進制檔案打包到單檔案應用程式中, |
| IncludeSymbolsInSingleFile | 將 .pdb 檔案打包到單個檔案中,提供該選項是為了和 .NET 3 單檔案模式兼容,建議替代的方法是生成帶有嵌入式的 PDB ( |
| IncludeAllContentInSingleFile | 將所有發布的檔案(符號檔案除外)打包到單檔案中,該選項提供是為了向后兼容 .NETCore 3.x 版本 |
組態檔設定引數
除了可以使用命令列引數的形式,還可以通過組態檔的形式設定發布引數,編輯專案檔案,添加配置節點到檔案中并保存即可,
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<PublishSingleFile>true</PublishSingleFile>
<IncludeContentInSingleFile>true</IncludeContentInSingleFile>
</PropertyGroup>
關于 RID 說明見:https://docs.microsoft.com/en-us/dotnet/core/rid-catalog
這是截止本文發布前的 RID 版本,不排除 .NET5.0 有新的發布
其它引數
除了上面的三個可選引數,我在查詢檔案的程序中還發現,官方還提到了其它引數的使用,目前不確定是否有效
<PropertyGroup>
<SelfContained>true</SelfContained>
<!--啟用使用assemby修剪-僅支持自包含應用程式-->
<PublishTrimmed> true </PublishTrimmed>
<!--啟用AOT編譯 目前暫不支持預編譯-->
<!--<PublishReadyToRun>true</PublishReadyToRun>-->
</PropertyGroup>
<ItemGroup>
<Content Update="*-exclute.dll">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
</ItemGroup>
還可以通過設定 ExcludeFromSingleFile 元素,該設定將指定某些檔案不嵌入單個檔案之中,
撰寫待打包的應用程式
為了更直觀的看出正常發布和單檔案發布的區別,我們特別準備了一個 Web 應用程式,并對兩個程式集進行依賴參考,

準備好專案,編譯成功,嘗試發布,打開 PowerShel 控制臺,分別輸入以下命令
dotnet publish -r linux-x64 /p:PublishSingleFile=true
dotnet publish -r win-x64 --self-contained=false /p:PublishSingleFile=true

linux-x64 和 win-x64 兩個目錄下,分別有 publish 目錄,由于平臺的不同,所參考的依賴也不一樣,這是我們早就了解過的,我們看看打包前后的區別

以上執行的兩條命令陳述句,會為我們生成 Linux 和 Windows 兩個平臺的程式包,從上圖中可以看出,在打包之前,專案的各種參考依賴都被復制到了發布目錄下,這也是我們之前的程式發布方式,在經過打包后,所有依賴檔案都被裝入了一個可執行檔案中,在 Linux 平臺下表現為:PreviewWebApplication ,Windows 平臺下則為:PreviewWebApplication.exe,從打包效果來看,遷移將變得更加方便了,
運行打包程式
打包后的程式和未打包的發布程式在運行方式上沒有太多的差異性,在 Windows 平臺上,只需要雙擊 PreviewWebApplication.exe 就可以運行該打包程式了,本示例創建的是一個 WebApi 的程式,直接訪問程式偵聽的地址后得到介面回傳的結果,如果您創建的是帶有 Razor 視圖或者攜帶其它資源檔案的,可能無法訪問指定的 url,

在程式成功運行起來后,我們發現,打包程式并沒有解壓縮檔案到磁盤,而是直接從包中加載檔案到記憶體中運行;這是巨大的進步,也是和 War 檔案根本的區別,
需要注意的是,該 .exe 檔案并不能單獨復制到別的地方運行,你必須把 .exe 當前目錄完整的復制才能運行,這涉及到主機探測的問題,下面我們將會一一提到,
跨平臺的打包檔案
通過上面的示例我們了解到,打包程式總是為不同的平臺生成獨立的包程式,這是為什么呢?這里就涉及到一個概念,也就是 Tool Interface Standard (TIS)
Executable and Linking Format(ELF)
Common Object File Format(COFF)于1983年引入,最初使用在 AT&T 的 UNIX 系統上,由于 COFF 的各種局限性,比如:節的最大數量受到限制,節名稱,所包含的源檔案的長度受到限制,并且符號除錯資訊無法支持實際的語言,最后,在 System V Release 4 (SVR4) 發布后,AT&T 使用 ELF 替代了 COFF,
工具介面標準委員會
援引委員會規范檔案的說明:可執行檔案和鏈接格式最初由 UNIX 系統開發和發布實驗室(USL)作為應用程式二進制介面(API)的一部分,工具介面標準委員會 (TIS) 選擇將不斷發展的 ELF 標準作為便攜式物件檔案,該標準適用于各種作業系統的 32 位英特爾架構環境的格式,ELF 標準旨在通過向開發人員提供具有一組跨多個操作環境的二進制介面定義,這將減少不同介面實作的數量,從而減少需要重新撰寫和編譯的代碼,
ELF 檔案結構又分為三種型別,分別是:
| 名稱 | 說明 | 描述 |
|---|---|---|
| 可重定位檔案 | Relocatable File | 包含適合與其他物件檔案鏈接的代碼和資料,以創建可執行檔案或共享物件檔案, |
| 可執行檔案 | Executable File | 包含適合執行的程式 |
| 共享目標檔案 | Shared Object File | 包含適合在兩種背景關系中鏈接的代碼和資料,首先,鏈接編輯器可以處理它與其他可重新洗掉和共享的物件檔案,以創建另一個物件檔案,其次,動態聯結器將其與可執行檔案和其他共享物件相結合,以創建行程映像, |
Portable Executable (PE)
在 Windows 陣營,微軟在此 COFF 標準的基礎上,又進行了創新和發展出了 PE 檔案標準
PE Format
該規范描述了Windows作業系統家族下的可執行檔案(影像)和目標檔案的結構,這些檔案分別稱為可移植可執行(PE)和公用物件檔案格式(COFF)檔案,
從上面的兩種規范中可以看出,LinuX 和 Windows 都有各自的檔案格式規范,而這種規范在一定程度上是不兼容的,不論是從檔案結構還是決議方式;所以 .NET5.0 中的打包程式必須為不同的平臺實作獨立的打包器,打包器的實作在 runtime 中的 Microsoft.NET.HostModel 庫中,
認識了 ELF 和 PE 檔案結構之后,我們就可以對打包器代碼進行閱讀理解,
Microsoft.NET.HostModel
你可以從 github 上下載 .NET 5.0 的源代碼,
轉到目錄:
runtime/src/installer/managed/Microsoft.NET.HostModel
原始碼不太多,可直接進行閱讀,主要理解層次關系即可,
打包器主要包含了三大部分的內容,分別是 AppHost、Bundler、ComHost
| 模塊 | 說明 |
|---|---|
| AppHost | 用于單檔案主機啟動時的檔案探測,還復制將程式資源從 App.dll 復制到 AppHost備用,目前已通過 HostFxr 和 HostPolicy 進行靜態鏈接,其探測邏輯已轉移到 HostPolicy(由C++撰寫) |
| Bundler | 打包器的具體實作,主要是將應用程式及其依賴項嵌入 AppHost 中,隨后發布單個可執行檔案到指定目錄 |
| ComHost | 創建一個包含嵌入式 CLSIDMap 檔案的 ComHost,以將 CLSID 映射到 .NET 類, |
在檔案 Bundle/Manifest.cs 的頭部,我們看到了“單檔案程式”的檔案結構定義
BundleManifest is a description of the contents of a bundle file.
This class handles creation and consumption of bundle-manifests.
Here is the description of the Bundle Layout:
_______________________________________________
AppHost
------------Embedded Files ---------------------
The embedded files including the app, its
configuration files, dependencies, and
possibly the runtime.
------------ Bundle Header -------------
MajorVersion
MinorVersion
NumEmbeddedFiles
ExtractionID
DepsJson Location [Version 2+]
Offset
Size
RuntimeConfigJson Location [Version 2+]
Offset
Size
Flags [Version 2+]
- - - - - - Manifest Entries - - - - - - - - - - -
Series of FileEntries (for each embedded file)
[File Type, Name, Offset, Size information]
_________________________________________________
從上面的檔案結構中,我們可以非常清晰的看到,單檔案程式的結構一共分為三大部分,分別是:
| 定義 | 說明 | 描述 |
|---|---|---|
| 嵌入的檔案 | Embedded file | 主要是組態檔和描述檔案,比如 .deps.json,runtimeconfig.json 等檔案 |
| 打包檔案頭資訊 | Bundle Header | 描述了整個檔案的結構資訊,型別,存盤位置,段、表等資訊 |
| 物體清單 | Manifest Entries | 實際打包的檔案串列,每個檔案分段寫入,可執行檔案使用 16byte - prev file end position 進行分隔,普通檔案直接按 prev file end position 進行寫入 |
檔案頭資訊的查看
我們可以通過一些工具去查看已經打包好的檔案,在 Linux 下,可以使用 readelf/objdump 等程式來獲取 PreviewWebApplication 檔案的資訊,在 Windows 下,可以使用 PE Tools 等工具
Linux 下 readelf 讀取檔案頭資訊

從圖中我們可以看到 Type:DYN (Shared object file) 這是一個標準的共享物件檔案,關于 ELF 頭部資訊的內容不再展開,有興趣的同學可以自行學習相關內容,
Windows下 PE Tools 讀取檔案頭資訊

已經打包好的程式內部包含了 319(Linux)、Windows(359) 個檔案,Windows 版本在未打包前是 84.3MB,打包后是 69.8MB,最重要的是在運行時無需解壓縮,直接從 Bundle 中運行檔案,
檔案中的第三部分,也就是 “物體清單(Manifest Entries)的寫入代碼在 Bundle\Bundler.cs\AddToBundle
long AddToBundle(Stream bundle, Stream file, FileType type)
{
if (type == FileType.Assembly)
{
long misalignment = (bundle.Position % AssemblyAlignment);
if (misalignment != 0)
{
long padding = AssemblyAlignment - misalignment;
bundle.Position += padding;
}
}
file.Position = 0;
long startOffset = bundle.Position;
file.CopyTo(bundle);
return startOffset;
}
在成員方法 GenerateBundle(IReadOnlyList
// 代碼片段
public string GenerateBundle(IReadOnlyList<FileSpec> fileSpecs)
{
...
foreach (var fileSpec in fileSpecs)
{
string relativePath = fileSpec.BundleRelativePath;
...
using (FileStream file = File.OpenRead(fileSpec.SourcePath))
{
FileType targetType = Target.TargetSpecificFileType(type);
long startOffset = AddToBundle(bundle, file, targetType);
FileEntry entry = BundleManifest.AddEntry(targetType, relativePath, startOffset, file.Length);
Tracer.Log($"Embed: {entry}");
}
}
// Write the bundle manifest
headerOffset = BundleManifest.Write(writer);
...
}
因為解壓器的實作已經轉移到了 HostFxr 和 HostPolicy 中,以靜態鏈接庫的方式鏈接到打包器中,且該部分代碼由 C++ 進行撰寫,鑒于 C++ 水平有限,在這里不作介紹,
結束語
撰寫這篇文章耗費了我大量的時間,期間大量閱讀海量的參考資料、文獻、標準檔案、制作文章配圖等等,寫干貨文章真的需要投入巨大的精力和時間,希望你們喜歡,
文章進行到這里,我知道肯定還有很多同學沒看過癮,但是我們可以通過回顧打包器的開發進度表來體驗一下 .NET 團隊的開發熱情,

主要參考資料
.NET團隊計劃經理 Richard Lander 的博客:https://devblogs.microsoft.com/dotnet/announcing-net-5-0-preview-8/
Bundler 進度表:https://github.com/dotnet/runtime/issues/36590
single-file:https://github.com/dotnet/designs/tree/master/accepted/2020/single-file
ELF檔案:https://refspecs.linuxbase.org/elf/elf.pdf
ELF維基百科:https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
Readelf:https://sourceware.org/binutils/docs/binutils/readelf.html
PE檔案:https://docs.microsoft.com/en-us/windows/win32/debug/pe-format
PE Tools:https://github.com/petoolse/petools
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/43.html
標籤:.NET Core
