.NET5.0 单文件发布打包操作深度剖析

可选参数
属性 描述
IncludeNativeLibrariesInSingleFile 在发布时,将依赖的本机二进制文件打包到单文件应用程序中。
IncludeSymbolsInSingleFile 将 .pdb 文件打包到单个文件中。提供该选项是为了和 .NET 3 单文件模式兼容。建议替代的方法是生成带有嵌入式的 PDB (embedded)的程序集
IncludeAllContentInSingleFile 将所有发布的文件(符号文件除外)打包到单文件中。该选项提供是为了向后兼容 .NETCore 3.x 版本
配置文件设置参数
除了可以使用命令行参数的形式,还可以通过配置文件的形式设置发布参数,编辑项目文件,添加配置节点到文件中并保存即可。

net5.0 linux-x64 true true 关于 RID 说明见:https://docs.microsoft.com/en-us/dotnet/core/rid-catalog

这是截止本文发布前的 RID 版本,不排除 .NET5.0 有新的发布

其它参数
除了上面的三个可选参数,我在查询文档的过程中还发现,官方还提到了其它参数的使用,目前不确定是否有效

true true PreserveNewest true 还可以通过设置 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 fileSpecs) 内部迭代调用了 AddToBundle 方法,完成了实体清单文件的写入。

// 代码片段

public string GenerateBundle(IReadOnlyList 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);

}
Absorbing material: www.goodsmaterial.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值