.NET5.0 單文件發布打包操作深度剖析

.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 (embedded)的程式集
IncludeAllContentInSingleFile 將所有發布的文件(符號文件除外)打包到單文件中。該選項提供是為了向後兼容 .NETCore 3.x 版本

配置文件設置參數

除了可以使用命令行參數的形式,還可以通過配置文件的形式設置發布參數,編輯項目文件,添加配置節點到文件中並保存即可。

<PropertyGroup>
  <TargetFramework>net5.0</TargetFramework>
  <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
  <PublishSingleFile>true</PublishSingleFile>
  <IncludeContentInSingleFile>true</IncludeContentInSingleFile>
</PropertyGroup>

關於 RID 說明見://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、Boundler、ComHost

模組 說明
AppHost 用於單文件主機啟動時的文件探測,還複製將程式資源從 App.dll 複製到 AppHost備用,目前已通過 HostFxr 和 HostPolicy 進行靜態鏈接,其探測邏輯已轉移到 HostPolicy(由C++編寫)
Boundler 打包器的具體實現,主要是將應用程式及其依賴項嵌入 AppHost 中,隨後發布單個可執行文件到指定目錄
ComHost 創建一個包含嵌入式 CLSIDMap 文件的 ComHost,以將 CLSID 映射到 .NET 類。

在文件 Boundle/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,最重要的是在運行時無需解壓縮,直接從 Boundle 中運行文件。

文件中的第三部分,也就是 「實體清單(Manifest Entries)的寫入程式碼在 Boundle\Boundler.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 fileSpecs) 內部迭代調用了 AddToBundle 方法,完成了實體清單文件的寫入。

// 程式碼片段

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 的部落格://devblogs.microsoft.com/dotnet/announcing-net-5-0-preview-8/
Boundler 進度表://github.com/dotnet/runtime/issues/36590
single-file://github.com/dotnet/designs/tree/master/accepted/2020/single-file
ELF文檔://refspecs.linuxbase.org/elf/elf.pdf
ELF維基百科://en.wikipedia.org/wiki/Executable_and_Linkable_Format
Readelf://sourceware.org/binutils/docs/binutils/readelf.html
PE文檔://docs.microsoft.com/en-us/windows/win32/debug/pe-format
PE Tools://github.com/petoolse/petools