.NET Core 程序的打包与分发 (Linux 篇 Part. III)

前两篇文章介绍了 .NET Core 程序在 Linux 平台上的两种部署方式。对于服务器程序,使用 .NET Core 的两种部署方式基本上足以满足需求,但是在很多情况下, .NET Core 仍然不是最佳的选择。

如果你想使用 .NET Core SDK,或者使用 .NET Standard,同时又有以下需求之一:

  1. 在非 x86/amd64 架构上部署程序,比如部署到 ARM 或者 MIPS 架构
  2. 在 Linux 平台上做开发,但部署的目标包含 FreeBSD 等 .NET Core 并未官方支持的平台
  3. .NET Core 2.0 仍未支持你所需要的 .NET Framework API,但 mono 却有较好的支持
  4. .NET Core 2.0 对你所需要的发行版支持仍然非常差

那么可以考虑使用 .NET Core SDK 完成开发,但是使用 mono runtime 作为最终的应用分发方式。

除此之外,使用 .NET Core SDK 搭配 mono runtime 使用,还能在 Linux 上生成 Windows 的 .NET Framework 程序。

需要注意的是,这种方式虽说是可行且受支持的,但是在官方文档里并没有被提及,只在 GitHub issue 中提到过。可能使用 mono 的 reference assembly 算是一种 hack。

环境

开发环境与部署环境均为 Arch Linux。前者安装 FPM.NET Core SDKmono

选择 Arch Linux 是因为 .NET Core 2.0 目前对其有非常好的支持,并且发行版软件源里 mono 版本足够新,不像其它常用发行版那样需要从第三方源(Xamarin)安装 mono。

示例程序

本文仍然沿用前文的 sslver 示例(源码)。这次与前文不同的是,需要修改 csproj 文件。

mkdir ~/sslver
cd ~/sslver
dotnet new console

修改 Program.cs 为示例程序后,编辑 sslver.csproj,将 TargetFrameworknetcoreapp2.0 改为 net462
以下为修改后的 csproj 文件:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net462</TargetFramework>
  </PropertyGroup>
</Project>

构建

尝试直接使用 dotnet build 命令,会报错:
/opt/dotnet/sdk/2.0.0/Microsoft.Common.CurrentVersion.targets(1122,5): error MSB3644: The reference assemblies for framework ".NETFramework,Version=v4.6.2" were not found. To resolve this, install the SDK or Targeting Pack for this framework version or retarget your application to a version of the framework for which you have the SDK or Targeting Pack installed. Note that assemblies will be resolved from the Global Assembly Cache (GAC) and will be used in place of refe:rence assemblies. Therefore your assembly may not be correctly targeted for the framework you intend. [/home/user/sslver/sslver.csproj]

这是因为 .NET Core SDK 找不到编译 .NET Framework 4.6.2 程序所需的 reference assembly。幸运的是,mono 提供了这些 reference assembly,我们可以直接在 .NET Core SDK 中使用它们。

使用命令 export FrameworkPathOverride=/usr/lib/mono/4.6.2-api/ 设置环境变量。

FrameworkPathOverride 用于覆盖默认的 .NET Framework reference assembly 目录,而 /usr/lib/mono/4.6.2-api/ 就是 mono 提供的 .NET Framework 4.6.2 reference assembly 的所在位置。

如果今后需要使用更新版本的 .NET Framework,只需修改 csproj 中的 TargetFramework 版本,和 FrameworkPathOverride 环境变量所指向的目录即可。

再次尝试 dotnet build -c=Release -f=net462 --output pack_root/opt/sslver/ ,输出:
sslver -> /home/user/sslver/pack_root/opt/sslver/sslver.exe
可以看到,成功地生成了 sslver.exe

不同于之前 .NET Core 所生成的 dll 文件,目标 .NET Framework 生成的是 exe 后缀的文件,可以直接在 Windows 平台上运行,也可以使用 mono 运行。

使用命令 mono pack_root/opt/sslver/sslver.exe 即可看到程序输出目前的 OpenSSL 版本: libcrypto 1.1.0f

打包

上面的步骤生成的文件全部位于 pack_root/opt/sslver 下。生成目标是 .NET Framework 时,生成的文件数量较少,本示例中只生成了 sslver.exesslver.pdb 两个文件,而我们只需要分发前者。

与共享 runtime 的 .NET Core 部署方式类似,为了能让 sslver 命令直接运行我们的程序,我们需要在 pack_root/usr/bin 下创建一个 stub 脚本:

mkdir -p pack_root/usr/bin
cat > pack_root/usr/bin/sslver <<__EOF__
#!/bin/sh
exec /usr/bin/mono /opt/sslver/sslver.exe "\$@"
__EOF__

设置权限

find pack_root/ -type d | xargs chmod 755
find pack_root/ -type f | xargs chmod 644
chmod +x pack_root/usr/bin/sslver

调用FPM完成打包

fpm -s dir -t pacman --name "sslver" -C pack_root/ --version 1.0.0 --iteration 1 --description "Displays OpenSSL Version" \
--depends mono --depends openssl \
--package ./

输出 Created package {:path=>"./sslver-1.0.0-1-x86_64.pkg.tar.xz"}

将文件移动到部署环境中,使用命令 pacman -U sslver-1.0.0-1-x86_64.pkg.tar.xz 安装,在安装了 mono 和一大坨依赖之后,就可以使用 sslver 命令检查打包是否成功了。

注意事项

  1. 虽说使用 .NET Core 生成 .NET Framework 或 mono 的可执行文件是可行的,但目前 .NET Core SDK 在非 Windows 平台下并不支持使用 dotnet run 调用 mono 运行 .NET Framework 目标的程序(相关issue);同时,dotnet test 也不支持使用 mono。也就是说基本只有 dotnet build 可以正常使用。
    虽说这些局限并不影响编译,但是却相当影响开发与测试。无法使用 SDK 自带的测试框架意味着编写测试代码等需要手动进行。而常用开发工具(如 Visual Studio Code)对 mono target 的支持也不是特别好。
    针对这一问题,我目前不推荐将 mono 作为唯一的 target,而是建议先用 .NET Core target 进行开发,再针对 .NET Framework 构建,在 mono runtime 下运行。
  2. 在 .NET Standard/.NET Core 下,我们通常使用使用 System.Runtime.InteropServices.RuntimeInformation 这个 API 来判断程序运行的平台。但是这个 API 在 .NET Framework 下是写死的返回 Windows,因此需要使用 Environment.OSVersion 这个 .NET Framework API 进行判断。如果你需要同时兼容 .NET Core 1.x 和 .NET Framework 而又不想用一大堆 #ifdef,那么可以参考这个答案
  3. 由于 API 实现在细节上有很大的不同,不要依赖于任何 API 的未定义行为,并且要针对目标环境进行完整的测试。
    比如在 .NET Framework/.NET Core 上,Socket 被 Dispose 后,LocalEndPoint/RemoteEndPoint 属性仍然可以被访问,但是在 mono 上,会直接抛出 ObjectDisposedException 异常。
  4. 部署环境中的 mono 一定要足够新,如果是 CentOS 或 Debian 之类的发行版,建议添加 mono 官方源

总结

使用 .NET Core SDK 开发,mono 分发和运行程序的情况并不常见,但考虑到目前 .NET Core 的平台支持仍然相当受限,如果你想开发一个非常流行的程序,那么还是建议支持 mono,并做好测试。

事实上,自从 Microsoft 把 .NET Framework 的 Reference Source 从 MSRSL 改成 MIT 开源,并且开发 .NET Core 后,mono 可以直接从 Microsoft 的开源项目中 copy 代码,因此实现的 API 在行为上与 .NET Framework 都更加接近了。同时,.NET Standard 2.0 也添加了大量原本仅存在于 .NET Framework 中的 API。我们可以推断,将来编写与维护同时支持 mono 和 .NET Core 的程序会越来越容易。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注