做 Avalonia 跨平台桌面开发的朋友,迟早会碰上一个现实问题:程序在 Windows 上跑得好好的,换成统信 UOS 就不知道怎么发给用户了。你总不能让人家先装 .NET SDK,再把 publish 出来的几十个文件手动复制到固定目录吧?最省心的方案就是打一个 .deb 安装包,让用户拿到手就能双击安装,装完启动器里出现图标,也能被系统统一管理、随时卸载。这篇文章就把我在统信 UOS 上把 Avalonia 程序打包成 deb 安装包的全过程讲一遍,包含发布模式怎么选、deb 内部结构怎么回事、control 文件怎么写、安装脚本怎么配,以及装完后常见的几个坑怎么排。适合已经能用 Avalonia 做出程序、正准备向统信或 deepin 环境分发的开发者,也适合团队里负责出包运维的同学参考。
1. 为什么要在统信系统上打 deb 包,以及准备工作
1.1 先认识一下 .deb 包到底是什么
Deb 是 Debian 系发行版通用的软件包格式,统信 UOS、deepin、Ubuntu 用的都是这一套。它的本质是一个 ar 归档文件,里面拆成了三块:debian-binary 记录格式版本,control.tar.gz 放包的控制信息,data.tar.gz 放实际要安装到系统的文件。安装的时候 dpkg 会把 data 部分解压到系统对应目录,然后执行 control 里的安装脚本,整个过程对用户来说就是双击一下的事。
有人可能会问:为什么不直接丢个 tar.gz 给用户?因为 tar.gz 只做了文件归档,依赖关系、桌面入口、图标注册、软件中心识别这些统统没有。而 deb 包是系统原生支持的格式,装上之后会被 dpkg 数据库记录,可以在统信的应用商店或者软件中心里被识别,卸载也干净。对政企内网来说,这种可追踪、可统一管理的分发方式比散装压缩包靠谱得多。
另外,deb 包还能声明依赖关系。比如你的 Avalonia 程序依赖 libfontconfig1、libgl1 这些系统库,写进 Depends 后,用户安装时如果缺了依赖,源里能自动拉取补齐,不会出现装上之后一启动就报缺库的尴尬。
1.2 打包前的环境检查与工具准备
统信的默认源里自带 dpkg 和 dpkg-deb,这两个是打包的核心工具,一般来说不需要额外安装。工作之前我习惯先看一眼系统信息和架构,命令如下:
cat /etc/os-version uname -m dpkg-deb --version dotnet --info架构很重要,统信 UOS 主要是 x86_64,但也存在 arm64、龙芯等版本。Avalonia 发布的时候 runtime identifier 要对得上,比如 x86_64 对应linux-x64,arm64 对应linux-arm64。我这边常用的环境是统信 UOS 20 专业版 (x86_64),打包出来的包名带 amd64 后缀。
如果统信系统上没有安装 .NET SDK,直接去 dotnet 官网下载对应架构的 tar.gz 包解压就行,不需要走系统源。解压之后把 dotnet 路径加进 PATH,再执行dotnet --info验证。这个环节踩过坑的人都知道,arm64 的机器下载 x64 的 SDK 是跑不起来的,务必确认架构配对。
提示:打包全过程建议用普通用户执行,然后通过 sudo 做安装验证,不要在 root 环境下直接操作文件。普通用户环境更贴近最终用户的使用状态,能提前暴露权限问题。
2. Avalonia 应用的 Linux 发布姿势
2.1 自包含还是框架依赖,我为什么选自包含
Avalonia 程序在 Linux 上的发布模式有两个方向:framework-dependent(框架依赖)和 self-contained(自包含)。框架依赖模式生成的包小,通常只有几 MB,但目标机器上必须装有对应版本的 .NET 运行时;自包含模式把整个 .NET 运行时也编进发布目录,体积会重不少,但目标机器上不需要预装任何 .NET 组件。
我在统信 UOS 上分发,一律建议自包含。原因很简单:统信系统本身自带的 dotnet 往往不是最新版本,不同机器上的运行时版本参差不齐。你辛辛苦苦写好的程序,不能把运行环境寄托在用户机器的运气上。自包含模式缺点是包大,一个 Avalonia 程序打出来通常有 70~100MB,但这年头硬盘和带宽都不差,换来的是"装上就能跑"的确定性。
2.2 发布命令与输出目录
发布命令我常用这条:
dotnet publish -c Release -r linux-x64 --self-contained true -o ./out/publish参数逐个说:-c Release是发布 Release 配置,-r linux-x64是运行时标识,--self-contained true强制自包含,-o指定输出目录。执行完之后,./out/publish下会有一个可执行的二进制文件(比如 MyApp),旁边跟着一大串 .dll、.json 文件,它们都要进安装包。
这里有个细节值得注意:不要轻易开启单文件发布(PublishSingleFile)。Avalonia 涉及 Skia 渲染等原生库,单文件模式下部分原生库的释放逻辑在 Linux 上容易出现临时文件残留问题,我见过多次装上之后莫名其妙起不来、或者个别功能失灵的反馈。老老实实用目录形式发布,虽然文件多,但稳定,出问题也好排查。
2.3 Avalonia 在 Linux 上离不开哪些系统库
Avalonia 是托管 UI 框架,但底层渲染和窗口管理还是绕不开系统原生库。打包之前心里要有这张依赖清单:
- libfontconfig1:字体配置与匹配,缺失则界面文字渲染异常
- libfreetype6:字体光栅化,直接关系到文字显示
- libice6、libsm6:X11 会话管理相关
- libgl1 / mesa:OpenGL 渲染路径,缺失会导致启动直接崩掉
- libgtk-3-0:部分文件对话框和系统集成功能需要 GTK
这些库最好写进 deb 的 Depends 字段,这样安装时统信会自动拉取补齐。别贪图省事不写依赖,等用户反馈"装完打不开"再去排查,成本比写一行依赖高得多。
3. .deb 包的结构解析与文件规范
3.1 deb 包打包前的目录长什么样
虽然 deb 安装包本身是 ar 归档,但我们手工打包时只需要组织一个"过渡目录"(staging directory),dpkg-deb 会自动把它压成标准格式。这个过渡目录结构大概是这样的:
MyApp_deb/ ├── DEBIAN/ │ ├── control │ ├── postinst │ └── postrm └── usr/ ├── lib/ │ └── myapp/ │ ├── MyApp │ └── ...(dll、json 等发布文件) └── share/ ├── applications/ │ └── myapp.desktop └── icons/ └── hicolor/ └── 512x512/ └── apps/ └── myapp.pngDEBIAN 目录(注意全大写)存放控制信息,不会安装到系统里去;usr 目录下的内容会原样铺到系统根目录的对应位置。也就是说,usr/lib/myapp/MyApp最终会出现在系统的/usr/lib/myapp/MyApp。
3.2 control 文件字段逐个说清楚
control 文件是整个包的身份证明,写错一点 dpkg 都会认账。我贴一个常用的模板:
Package: myapp Version: 1.0.0 Architecture: amd64 Maintainer: Your Name <you@example.com> Installed-Size: 10240 Depends: libfontconfig1, libice6, libsm6, libgl1, libgtk-3-0 Section: utils Priority: optional Description: My Avalonia Application A cross-platform Avalonia desktop application for UOS.字段很容易出问题,逐个说:
- Package:包名,只能用小写英文字母、数字和连字符。我用 MyApp 的驼峰写法打包时就踩过坑,dpkg 直接报 invalid characters。
- Version:版本号,建议和项目 tag 对齐,方便追溯。
- Architecture:只能写
amd64,不能写x86_64,这是 dpkg 的架构命名,写错了装上会提示架构不匹配。 - Installed-Size:单位是 KB,估算发布目录总大小就行,写大点没坏处。
- Depends:逗号分隔的依赖包列表,建议把 2.3 节的库都写进去。
- Description:第一行是简短描述,后续行必须以一个空格开头作为详细说明,缩进错了 control 文件就解析失败。
注意:control 文件必须用 UTF-8 编码保存,换行用 LF。Windows 上编辑过再拷到 Linux 容易带上 CRLF,dpkg 会报奇怪的语法错误。
3.3 安装脚本 postinst 与 postrm 的职责划分
postinst 在系统文件全部复制完成之后执行,体积不大但作用很关键。它通常干三件事:给主程序加执行权限、在 /usr/local/bin 下建一个软链方便命令行调用、刷新桌面数据库和图标缓存。postrm 是卸载时执行的反向脚本,主要清理软链。两个脚本都要以#!/bin/bash开头,并且文件要有可执行权限,否则 dpkg 会无视它们。
还要在 postinst 里注意一点:脚本执行失败会直接导致整个安装事务回滚,所以开头加set -e很有必要。刷新图标缓存这种操作偶尔会因环境问题失败,可以在末尾加上|| true兜底,别让一个缓存刷新失败毁掉整个安装流程。
4. 全流程实操:从 publish 到 dpkg -i
4.1 建立打包工作目录并拷贝发布文件
现在开始动真格的。先建一个干净的目录作为打包根,然后把第二步 publish 出来的所有文件完整拷贝进去。
mkdir -p MyApp_deb/usr/lib/myapp mkdir -p MyApp_deb/usr/share/applications mkdir -p MyApp_deb/usr/share/icons/hicolor/512x512/apps cp -a ./out/publish/* MyApp_deb/usr/lib/myapp/ chmod +x MyApp_deb/usr/lib/myapp/MyApp拷贝务必用-a保留文件属性,因为部分文件可能带只读标记,丢了权限后面会引发奇怪的问题。主程序和 .so 文件尤其要保证可执行权限。
4.2 写 .desktop 桌面入口文件
统信 DDE 桌面环境通过 .desktop 文件识别应用图标,这一步不做,装完包启动器里什么都看不到。文件内容如下:
[Desktop Entry] Type=Application Name=MyApp Comment=My Avalonia Application Exec=/usr/lib/myapp/MyApp Icon=/usr/share/icons/hicolor/512x512/apps/myapp.png Terminal=false Categories=Utility;Office; StartupNotify=trueExec 一定要写绝对路径,别写相对路径,也别指望系统会自动搜 PATH。Icon 同样用绝对路径指向 deb 里带进去的图标文件。图标建议用 512x512 的 PNG,开发环境下直接拿应用图标导出成 png 放到指定目录即可。Categories 可以按需写,Utility;Office;这组合在统信启动器里能被正常归类和搜索到。
4.3 亲手写 control 和安装脚本
两个目录结构准备好后,开始写 DEBIAN 下的三个文件。control 直接用 3.2 节的模板,把包名和版本改掉就行,not going to repeat。postinst 我一般这么写:
#!/bin/bash set -e chmod +x /usr/lib/myapp/MyApp ln -sf /usr/lib/myapp/MyApp /usr/local/bin/myapp update-desktop-database /usr/share/applications/ >/dev/null 2>&1 || true gtk-update-icon-cache /usr/share/icons/hicolor/ >/dev/null 2>&1 || truepostrm 就干一件事:
#!/bin/bash set -e rm -f /usr/local/bin/myapp写完之后别忘给这两个脚本加执行权限:
chmod +x MyApp_deb/DEBIAN/postinst MyApp_deb/DEBIAN/postrm这一步漏了,安装时 postinst 脚本不会执行,届时你会发现装完包没有可执行按钮、启动器里也没有图标。
4.4 用 dpkg-deb 打包并验证内容
目录结构完整、脚本权限到位之后,执行打包命令:
dpkg-deb --build --root-owner-group MyApp_deb MyApp_1.0.0_amd64.deb--root-owner-group很重要,它把包内文件的 owner 强制设为 root,不然打出来的包里文件 owner 是你当前用户的 UID,其他用户装上之后可能出现文件属主混乱。打包完成后用下面两条命令检查内容:
dpkg-deb --info MyApp_1.0.0_amd64.deb dpkg-deb -c MyApp_1.0.0_amd64.deb--info看 control 信息,-c列出内含文件清单。我习惯在正式发布之前把这两步输出都过一遍,确认版本号、依赖、关键文件都在。
5. 安装测试与常见问题排查
5.1 安装卸载验证流程
打包不是终点,装上能用才是终点。测试机上执行安装:
sudo dpkg -i MyApp_1.0.0_amd64.deb如果系统源里有缺失依赖,dpkg 会提示依赖未满足,这时执行sudo apt --fix-broken install自动补齐,然后重新 dpkg -i。装完确认包已注册:
dpkg -l | grep myapp然后去启动器里搜索 MyApp,点开跑一跑,确认主流程正常。卸载测试也建议跑一遍:
sudo dpkg -r myapp卸载后检查/usr/local/bin/myapp软链是否被清理干净,桌面图标是否消失。来回迭代验证没问题,这个包才能对外发。
5.2 我踩过的六个坑
打包这事儿看着简单,实际操作里坑一个接一个。我把最有代表性的六个列出来:
- Package 字段用了大写字母:dpkg 直接报"package name has invalid characters"。包名永远用小写。
- .desktop 文件里 Exec 写的相对路径:启动器图标点了没反应,改成绝对路径立刻好。
- 忘了给 MyApp 主程序加执行权限:安装正常,但双击无反应,命令行直接提示 Permission denied。
- control 文件 Windows 编辑带 CRLF:dpkg --build 不报错,安装时提示"invalid control file"。写完后用
file命令看一眼格式最保险。 - 架构写成 x86_64:安装提示 architecture mismatch,改成 amd64 才通过。
- 图标路径不匹配:图标实际在 512x512 目录,.desktop 却指向其他尺寸,启动器里只显示默认齿轮图标。
5.3 装上之后启动黑屏或者闪退怎么办
这个问题在统信 UOS 上遇到得不算少,通常和图形环境或者渲染库有关。统信 DDE 默认可能跑在 X11,也可能跑在 Wayland,而 Avalonia 程序在不同后端下的表现并不一致。如果启动黑屏,先检查系统是否装了 mesa 等 OpenGL 库,缺了直接补上:
sudo apt install mesa-utils libgl1如果补了图形库还黑屏,可以试试在 .desktop 文件的 Exec 行后面追加环境变量强制 X11 后端:
Exec=env DISPLAY=:0 /usr/lib/myapp/MyApp这个变量对部分用户环境有效,但最有效的方法是在程序里手动指定 Avalonia 的 backend 优先级。在 Main 函数里可以使用 AppBuilder 配置 X11 优先:
.UseX11()这样至少能保证在没有 Wayland 支持的情况下程序能落到 X11 路径,不至于一启动就黑。
结尾再分享一个习惯:我实际用这套流程出过几次包之后,就把 publish 和打包整个过程写成了一个 build-deb.sh 脚本放在仓库的 build 目录下,每次发版只需要改版本号和包名,一条命令出包。包里文件多了之后,还可以顺便在脚本里把 md5sums 生成出来放进 DEBIAN 目录,方便 dpkg 完整性校验。用 dpkg-deb 手工打 deb 包并不难,难的是把流程规范化和自动化。把这套沉淀下来,后续不管出多少个统信版本,都是改个版本号的事。