【技术】Flatpak打包dotnet程序踩坑实录

起因是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.dotnet9org.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的通用性,但只是

能不能把文档写好点啊!!!

本文遵循CC-BY-SA 4.0协议
暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇