feat(release): build and verify an AppImage of Studio on Linux (download only)

This commit is contained in:
esengine committed 2026-10-18 03:25:38 -07:00
1 parent edcbc47c06
commit d136ac2828
11 files changed
+94 -10

No files matched your search

+9 -1
View File
@@ -389,6 +389,14 @@ jobs:
version="${{ needs.resolve.outputs.version }}"
bash scripts/linux-install-smoke.sh "dist/ReasonixStudio-linux-$arch.deb" "$arch" "${version#v}"
- name: Verify the AppImage
if: runner.os == 'Linux'
shell: bash
run: |
platform="${{ matrix.platform }}"
arch="${platform#linux/}"
bash scripts/verify-linux-appimage.sh "dist/ReasonixStudio-linux-$arch.AppImage" "$arch"
# With signing on, this leg only hands the bundle on: the signing jobs
# run nothing but the signer, and windows-package packages in between.
- name: Upload the unsigned Windows bundle for signing
@@ -1199,7 +1207,7 @@ jobs:
fi
{
printf '%s\n\n' '---'
printf "**macOS** 已用 Developer ID 签名并公证,打开时不需要清除隔离属性。%s**Linux** Debian 系从 \`.deb\` 安装,Fedora、RHEL、openSUSE 等从 \`.rpm\` 安装(\`sudo dnf install ./ReasonixStudio-linux-amd64.rpm\`)。x86_64 用文件名带 amd64 的包,aarch64(统信 UOS、麒麟等)用带 arm64 的包。\n\n" "$windows"
printf "**macOS** 已用 Developer ID 签名并公证,打开时不需要清除隔离属性。%s**Linux** Debian 系从 \`.deb\` 安装,Fedora、RHEL、openSUSE 等从 \`.rpm\` 安装(\`sudo dnf install ./ReasonixStudio-linux-amd64.rpm\`)。x86_64 用文件名带 amd64 的包,aarch64(统信 UOS、麒麟等)用带 arm64 的包。也提供 \`.AppImage\`(只提供下载,不会自更新;需要 FUSE 2,或用 \`--appimage-extract-and-run\` 启动)。\n\n" "$windows"
printf "%s\n" "三个平台都能自更新:Linux 的 \`.deb\` 走自己的包(\`.rpm\` 的自更新尚未提供,请手动下载新包),Windows 运行下一个安装器,macOS 替换自身 bundle。"
} >> "$notes"
assets=()
+5 -1
View File
@@ -363,7 +363,7 @@ jobs:
arm64=false
platform=$(git diff -z --name-only --no-renames "$BASE_SHA" HEAD | bash scripts/ci-platform-scope.sh)
if git diff --name-only --no-renames "$BASE_SHA" HEAD |
grep -qE '^(desktop/electron/|desktop/packaging/|desktop/cmd/update-helper/|desktop/cmd/studio-manifest/|desktop/go\.(mod|sum)$|cmd/reasonix-studio-host/|internal/platform/(update|appupdate)/|go\.(mod|sum)$|scripts/(linux-install-smoke|verify-linux-package|verify-studio-artifacts)\.sh$|scripts/(windows-signing-lib|verify-windows-package|verify-windows-authenticode|install-smoke)\.ps1$|\.github/workflows/(studio|release-studio)\.yml$)'; then
grep -qE '^(desktop/electron/|desktop/packaging/|desktop/cmd/update-helper/|desktop/cmd/studio-manifest/|desktop/go\.(mod|sum)$|cmd/reasonix-studio-host/|internal/platform/(update|appupdate)/|go\.(mod|sum)$|scripts/(linux-install-smoke|verify-linux-package|verify-linux-appimage|verify-studio-artifacts)\.sh$|scripts/(windows-signing-lib|verify-windows-package|verify-windows-authenticode|install-smoke)\.ps1$|\.github/workflows/(studio|release-studio)\.yml$)'; then
arm64=true
fi
fi
@@ -607,6 +607,10 @@ jobs:
if: runner.os == 'Linux'
run: bash scripts/linux-install-smoke.sh "dist/ReasonixStudio-linux-${{ matrix.arch }}.deb" "${{ matrix.arch }}" 0.0.0-ci
- name: The AppImage carries its architecture, the kernel and the page
if: runner.os == 'Linux'
run: bash scripts/verify-linux-appimage.sh "dist/ReasonixStudio-linux-${{ matrix.arch }}.AppImage" "${{ matrix.arch }}"
# Recorded, not asserted: the real bundle layout, so later work argues
# from what electron-builder produced rather than from its documentation.
- name: Record the macOS bundle layout
+1 -1
View File
@@ -151,7 +151,7 @@ pre-releases):
| --- | --- | --- |
| macOS | `.dmg` or `.zip` | Apple Silicon (`arm64`) / Intel (`amd64`) |
| Windows | Installer `-installer.exe` or portable `.zip` | x64 |
| Linux | `.deb` or `.rpm` | x64 (`amd64`) / ARM64 (`arm64`, aarch64) |
| Linux | `.deb`, `.rpm`, or `.AppImage` (download only) | x64 (`amd64`) / ARM64 (`arm64`, aarch64) |
Every package ships with a `.minisig` signature, and the release carries
`SHA256SUMS`. Once installed, Studio updates itself in place.
+1 -1
View File
@@ -139,7 +139,7 @@ Reasonix 分为两条版本线,调整的原因见[版本路线公告](https://
| --- | --- | --- |
| macOS | `.dmg` 或 `.zip` | Apple Silicon(`arm64`)/ Intel(`amd64`) |
| Windows | 安装器 `-installer.exe` 或便携 `.zip` | x64 |
| Linux | `.deb` 或 `.rpm` | x64(`amd64`)/ ARM64(`arm64`,aarch64) |
| Linux | `.deb`、`.rpm` 或 `.AppImage`(只提供下载) | x64(`amd64`)/ ARM64(`arm64`,aarch64) |
每个安装包都附带 `.minisig` 签名,发布页另有 `SHA256SUMS`。安装后 Studio 会在
应用内自行更新。
+6 -4
View File
@@ -135,14 +135,16 @@ func releaseAsset(repo, tag, name string, mirrored bool) update.Asset {
}
}
// installable reports whether an artifact installs itself rather than expecting
// the reader to place it: a disk image to drag from, an installer to run, a
// package for dpkg.
// installable reports whether an artifact is a download a person is offered: a
// disk image to drag from, an installer to run, a package for dpkg or rpm, or an
// AppImage to run in place. The AppImage is a download only; no native_packages
// key names it, so no client replaces one.
func installable(name string) bool {
return strings.HasSuffix(name, ".dmg") ||
strings.HasSuffix(name, "-installer.exe") ||
strings.HasSuffix(name, ".deb") ||
strings.HasSuffix(name, ".rpm")
strings.HasSuffix(name, ".rpm") ||
strings.HasSuffix(name, ".AppImage")
}
// macArchiveKeys maps a macOS archive to the platforms it can install. The
+33
View File
@@ -340,3 +340,36 @@ func TestLinuxArm64PackagesAreKeyedBesideAmd64(t *testing.T) {
}
}
}
// An AppImage is a download a person is offered and nothing an updater
// replaces: it must not appear as a platform asset or a native package, and
// adding it must not move any entry a shipped client reads.
func TestAnAppImageIsOfferedButNeverResolvedAsAnUpdate(t *testing.T) {
dir := t.TempDir()
for _, name := range []string{"ReasonixStudio-linux-amd64.deb", "ReasonixStudio-linux-amd64.AppImage", "ReasonixStudio-linux-arm64.AppImage"} {
if err := os.WriteFile(filepath.Join(dir, name), []byte(name), 0o644); err != nil {
t.Fatal(err)
}
}
t.Setenv("GITHUB_REPOSITORY", "esengine/DeepSeek-Reasonix")
if err := run(dir, "v0.1.0", "studio-v0.1.0", "", true); err != nil {
t.Fatalf("run: %v", err)
}
var m update.Manifest
raw, _ := os.ReadFile(filepath.Join(dir, "latest.json"))
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
for _, name := range []string{"ReasonixStudio-linux-amd64.AppImage", "ReasonixStudio-linux-arm64.AppImage"} {
a, ok := m.Downloads[name]
if !ok || a.Sig != a.URL+".minisig" {
t.Errorf("downloads[%s] = %+v, %v; want a signed download", name, a, ok)
}
}
if len(m.NativePackages) != 1 || !strings.HasSuffix(m.NativePackages["linux-amd64"].URL, "linux-amd64.deb") {
t.Errorf("native packages = %+v, want only the .deb", m.NativePackages)
}
if len(m.Platforms) != 0 {
t.Errorf("platforms = %+v: an AppImage must not resolve as an update", m.Platforms)
}
}
+2
View File
@@ -99,6 +99,8 @@ linux:
target:
- deb
- rpm
# Download only: it does not update itself, and nothing here installs it.
- AppImage
icon: assets/icon.png
category: Development
maintainer: esengine <[email protected]>
+2 -1
View File
@@ -288,7 +288,8 @@ Studio and 1.x signing jobs and smoke tests share the concurrency group `certum-
`latest.json` keys `platforms` (and `deltas`) as `<GOOS>-<GOARCH>`, derived from the artifact name by `studio-manifest` and asked for by the running kernel through `runtime.GOOS` and `runtime.GOARCH`.
- The key is the binary's own architecture, not the machine's. An x64 build running under ARM64 emulation asks for `windows-amd64` and keeps updating to the amd64 installer; it is not moved to a native build.
- Linux ships a `.deb` and an `.rpm` for each of amd64 and arm64 (aarch64), all named `ReasonixStudio-linux-<arch>.<ext>`; the publish job requires all four by name.
- Linux ships a `.deb`, an `.rpm` and an `.AppImage` for each of amd64 and arm64 (aarch64), all named `ReasonixStudio-linux-<arch>.<ext>`; the publish job requires all six by name.
- The `.AppImage` is a download only: no `native_packages` key names it and no client replaces one, so an AppImage user downloads the next one. `scripts/verify-linux-appimage.sh` checks its type marker, its ELF machine, and that extracting it yields the kernel (for the right architecture) and the page.
- CI and releases use `scripts/verify-linux-package.sh` to read both formats back: package name, architecture, root-owned update payload, every ELF image, and static Go binaries.
- `scripts/linux-install-smoke.sh` runs only with `GITHUB_ACTIONS=true`. It installs and reinstalls the `.deb`, drives its packaged Electron window under Xvfb through a renderer-to-kernel request and second launch, checks normal shell/kernel exit, and uninstalls.
- ARM64 packaging does not establish compatibility with every UOS/Kylin version. Qualify the target distribution's glibc, GTK/NSS, graphics/session environment and, for `.deb` self-update, apt, pkexec and Polkit. RPM updates remain manual.
@@ -0,0 +1,2 @@
- 新增:Linux 增加 `.AppImage` 下载(amd64 和 arm64),可直接运行;它不会自更新,新版本需重新下载。关联 #11653。
- Added: Linux gets an `.AppImage` download for amd64 and arm64 that runs in place. It does not update itself: download the next release. Refs #11653.
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
appimage=${1:?usage: verify-linux-appimage.sh APPIMAGE ARCH}
arch=${2:?usage: verify-linux-appimage.sh APPIMAGE ARCH}
case "$arch" in
amd64) machine=62 ;;
arm64) machine=183 ;;
*) echo "unsupported architecture: $arch" >&2; exit 1 ;;
esac
test -s "$appimage"
# An AppImage is an ELF runtime with a type marker at offset 8: "AI" and the type
# byte 2, the squashfs flavour. Offset 18 is the ELF machine, little-endian.
test "$(dd if="$appimage" bs=1 count=4 2>/dev/null | od -An -tx1 | tr -d ' \n')" = 7f454c46
test "$(dd if="$appimage" bs=1 skip=8 count=3 2>/dev/null | od -An -tx1 | tr -d ' \n')" = 414902
test "$(dd if="$appimage" bs=1 skip=18 count=1 2>/dev/null | od -An -tu1 | tr -d ' \n')" = "$machine"
stage=$(mktemp -d)
trap 'rm -rf -- "$stage"' EXIT
cp "$appimage" "$stage/app"
chmod +x "$stage/app"
# Extracting reads the image itself, so it needs neither FUSE nor a display.
(cd "$stage" && ./app --appimage-extract >/dev/null)
root=$stage/squashfs-root
host=$(find "$root" -type f -path '*/bin/reasonix-studio-host' ! -path '*app.asar*' | head -1)
page=$(find "$root" -type f -path '*/frontend-next/dist/index.html' ! -path '*app.asar*' | head -1)
test -n "$host" -a -x "$host"
test -n "$page"
test "$(dd if="$host" bs=1 skip=18 count=1 2>/dev/null | od -An -tu1 | tr -d ' \n')" = "$machine"
test -e "$root/AppRun"
echo "AppImage ok: kernel ${host#"$root"/}, page ${page#"$root"/}"
+1 -1
View File
@@ -2,7 +2,7 @@
set -euo pipefail
dir=${1:?usage: verify-studio-artifacts.sh DIRECTORY}
for arch in amd64 arm64; do
for name in "ReasonixStudio-linux-$arch.deb" "ReasonixStudio-linux-$arch.rpm" \
for name in "ReasonixStudio-linux-$arch.deb" "ReasonixStudio-linux-$arch.rpm" "ReasonixStudio-linux-$arch.AppImage" \
"ReasonixStudio-windows-$arch-installer.exe" "ReasonixStudio-windows-$arch.zip" \
"ReasonixStudio-darwin-$arch.dmg" "ReasonixStudio-darwin-$arch.zip"; do
test -s "$dir/$name" || { echo "missing or empty Studio artifact: $name" >&2; exit 1; }