起因是ClassIsland在Linux平台上的打包是跟着系统软件包管理器走的,也就是说每一个软件包管理器都需要一个不同的打包,这样做极大的增加了所需要的后期维护成本。所以想用一种跨发行版通用的打包方式来简化在Linux平台上的软件分发流程。
经过一番研究后选择Flatpak。Flatpak标榜自己是“软件分发的未来”,这点比较见仁见智,但不可否认的是一些使用率高的软件也选择了Flatpak作为一种分发方式,例如OBS。其实只是因为好跨发行版分发才选择的
Flatpak之于Linux发行版就好像Linux内核之于各种各样的硬件一样,为应用程序提供了一种容器化的环境来统一应用程序的运行时环境(把Docker的目标从部署项目变为部署应用程序)。这样做为开发者节省了要适配各种显示协议(X11与Wayland)和不同桌面环境的通知等工作,尤其是避免了在Linux上碰到库依赖问题(因为打包了应用程序所需要的依赖)。
先说一下打包的这个项目情况:
- Avalonia跨平台应用程序 Windows/macOS/Linux三端支持
- 编译过程需要.NET 8和.NET 9两个SDK,但运行时只需要.NET 8 Runtime
- 应用程序可以直接在文件夹中运行,也可以打包成软件包运行
- 应用程序本身之前做过deb的打包,做过相关支持,可以将应用数据根据打包方式写入不同位置
- 仅X11支持(Avalonia原因)
乍看之下感觉应用程序本身这边支持已经比较完善了,所以接下来只需要看官方文档直接打包就行了,是吧?
当然是不可能了啦,不然也就不会有这篇文章了。
问题0:神秘的还原脚本
还原是每个dotnet项目在构建时都绕不过去的一关,实质是将项目所需要的第三方依赖下载,以“还原”成一个完整可编译的项目状态。在正常的项目编译时,只需要运行dotnet restore就大功告成了,甚至可以直接运行dotnet build让其自动还原后编译。但Flatpak就从这出岔子了。
Flatpak官方文档关于Dotnet应用程序的打包确实是一个不错的开始,提供了所有必须要求的步骤,但问题在于没有解释为什么要使用这个脚本?
在经过一些试错后我大致了解了Flatpak打包应用的原理:flatpak-builder会在一个容器内构建应用,该容器的权限无法被配置,而在这个容器里是没有网络的,这就导致传统的dotnet restore在连接nuget.org时会timeout错误,导致后续正常的编译进程无法进行。
因此,Flatpak官方提供了这个脚本来在容器外做项目的索引还原(即把项目依赖还原为一个个Nuget包链接),通过一个文件将还原信息传递,在后续flatpak-builder开始项目自定义编译前,把nuget包提前下载到容器中,因而手动完成了一次还原操作。
问题1:SDK混用怎么办?
官方文档里没有解释一些特殊情况,比如对于这种dotnet 8/9 SDK混用的项目怎么办?官方给出的打包配置是这么写的:
...
sdk-extensions:
- org.freedesktop.Sdk.Extension.dotnet8
build-options:
prepend-path: "/usr/lib/sdk/dotnet8/bin"
append-ld-library-path: "/usr/lib/sdk/dotnet8/lib"
prepend-pkg-config-path: "/usr/lib/sdk/dotnet8/lib/pkgconfig"
...
modules:
- name: dotnet
buildsystem: simple
build-commands:
- /usr/lib/sdk/dotnet8/bin/install.sh
...所以你会认为照猫画虎地在sdk-extensions加一个org.freedesktop.Sdk.Extension.dotnet9,把下面那些环境变量加一加,build-commands里再跑一个dotnet9的install.sh就好了吧?
恭喜你,会得到符号链接错误。问题出在加的那行dotnet9的install.sh。如果你像官方示例里提供的一样不使用自包含而是在dotnet module里运行dotnet8的install.sh,如果项目只需要一个SDK编译运行的话还好,但这个项目不行。因为这两个脚本是依赖符号链接的,后运行的install.sh无法覆盖前一个脚本所创建的符号链接。同时,外面的那个脚本也要指定最高dotnet版本是9(这样两个SDK就都能用了)。
同样也是这个install.sh的问题导致了dotnet运行时问题。两个sdk-extensions只是提供了能构建应用的工具,不代表就会自动把运行时也安装进bundle里,这两个install.sh才是把运行时装进bundle的关键。迫于无奈,我只能把编译过程改为自包含。然而官方文档是没有发布自包含的,相关文档只在org.freedesktop.Sdk.Extension.dotnet9和org.freedesktop.Sdk.Extension.dotnet8的README有,这块还有一个坑就是,不要只改完容器内的构建命令就跑,容器外的那个脚本也要指定运行时(不然会缺一些用来自包含的Nuget包)。
问题2:我git tag呢?
这个项目使用最后的git tag来作为每次编译的代号/版本号,相关信息通过gitinfo在编译时动态获取。然而在编译过程中会报错:
AssemblyInfo.cs: error CS7034: The specified version string 'fatal: No names found, cannot describe anything.' does not conform to the required format - major[.minor[.build[.revision]]]这就奇怪了,项目git clone下来是有tag的啊?
问题出现在flatpak-builder复制git仓库的过程中,好消息是它把所有submodule都fetch了,坏消息是没有任何tag啊,谁能想到这块会出问题?没办法,只能把sources里克隆项目代码的部分改下:
{
"type": "git",
"url": "../.."
}
改成
{
"type": "dir",
"path": "../..",
"dest": "."
}事已至此,自动化吧
综上所述,我决定写一个Flatpak构建用的bash脚本,来确保环境已配置和必要文件都存在,最终生成一个Flatpak bundle(即Flatpak软件包),用了些AI辅助 :3
#!/bin/bash
set -e # 遇到错误则立即终止脚本
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
FLATPAK_DOTNET_GENERATOR_URL="https://github.com/flatpak/flatpak-builder-tools/raw/refs/heads/master/dotnet/flatpak-dotnet-generator.py"
FLATPAK_MANIFEST="org.classisland.ClassIsland.json" # 指定flatpak-builder所使用的打包配置文件
echo ""
# 环境检查:Linux、flatpak和flatpak-builder命令都可用
if [ "$(uname -s)" != "Linux" ]; then
echo "Error: This script must be run on Linux."
exit 1
fi
echo " ✓ Running on Linux"
if ! command -v flatpak &>/dev/null; then
echo "Error: flatpak is not installed. Please install it first."
exit 1
fi
echo " ✓ flatpak is installed"
if ! command -v flatpak-builder &>/dev/null; then
echo "Error: flatpak-builder is not installed. Please install it first."
exit 1
fi
echo " ✓ flatpak-builder is installed"
if ! flatpak remotes | grep -q "^flathub"; then
echo " Flathub remote not found. Adding flathub..."
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
echo " ✓ Flathub remote added"
else
echo " ✓ Flathub remote is configured"
fi
# 手动还原
echo "Downloading flatpak-dotnet-generator.py..."
cd "$SCRIPT_DIR"
if [ -f "flatpak-dotnet-generator.py" ]; then
echo " flatpak-dotnet-generator.py already exists, skipping download"
else
if command -v curl &>/dev/null; then
curl -LO "$FLATPAK_DOTNET_GENERATOR_URL"
elif command -v wget &>/dev/null; then
wget "$FLATPAK_DOTNET_GENERATOR_URL"
else
echo "Error: Neither curl nor wget is available to download the script."
exit 1
fi
chmod +x flatpak-dotnet-generator.py
echo " ✓ flatpak-dotnet-generator.py downloaded"
fi
case $(uname -m) in
x86_64)
RUNTIME="linux-x64"
;;
aarch64|arm64)
RUNTIME="linux-arm64"
;;
*)
echo "Unsupported Runtime.Exiting..."
exit 1
;;
esac
echo "Generating Nuget source file for $RUNTIME..."
python3 flatpak-dotnet-generator.py sources.json "$SCRIPT_DIR/../../ClassIsland.Desktop/ClassIsland.Desktop.csproj" -d 9 -r $RUNTIME
echo " ✓ sources.json generated"
# 编译打包项目
echo "Building Flatpak package..."
flatpak-builder --install-deps-from=flathub --force-clean --repo=repo build "$FLATPAK_MANIFEST"
echo " ✓ Flatpak build completed"
# 导出打包项目
echo "Exporting Flatpak bundle..."
flatpak build-bundle repo ClassIsland.flatpak org.classisland.ClassIsland
echo " ✓ Flatpak bundle created: ClassIsland.flatpak"
echo ""
echo "Done!"
echo "Flatpak bundle location: $SCRIPT_DIR/ClassIsland.flatpak"到这,软件打包部分才算完成,剩下的就是与应用程序兼容性方面的调整了。这篇blog不是想批判Flatpak的复杂度,你甚至可以说正是这样的复杂度保证了Flatpak的通用性,但只是
能不能把文档写好点啊!!!