Files
tty7/.github/scripts/bundle-appimage.sh
T
webdev ac3c95a647 feat(update): install verified Linux AppImage releases in app (#306) (#652)
The last platform from #306: a Linux install running as an AppImage can
now download, verify, and apply a release from inside the app, through
the same tty7-updater helper the macOS (#309) and Windows (#330) paths
use. Tarball and distro installs are deliberately untouched — they keep
the named-package hint and the release page, because replacing a file a
package manager may own is not this code's call to make.

The installed artifact is one file, the path $APPIMAGE names, so the
install is the simplest of the three platforms: stage the download
beside the image (two renames only stay atomic on one filesystem),
verify, swap, relaunch, and restore the preserved previous image if the
new one does not survive its launch grace. What is Linux-shaped about
it is the mount: the image the GUI runs from is FUSE-mounted by the
AppImage runtime and torn down when the app exits, which is the moment
the installer starts working — so the GUI copies the helper out of the
mount into staging and runs the copy, the way the Windows path runs a
private copy because Setup replaces the installed one. The daemon is
left running throughout, as on macOS: nothing on Linux locks a running
executable's file, and the panes it serves are the reason the update
restarts only the GUI. The swap also carries the installed image's own
mode onto its replacement, so a 0700 image stays private and the
download's missing execute bit never reaches the installation.

Verification holds the issue's requirements with what an unsigned ELF
can offer: the bytes must match the release's checksums.txt, the file
must actually be a type-2 AppImage — a mis-published asset fails with a
name instead of at launch — and the image must state the version it
claims. That statement is new: bundle-appimage.sh stamps
X-AppImage-Version into the desktop entry, and the updater reads it
back with one --appimage-extract, answered by the runtime before any
application code and without FUSE. The same pass requires the new image
to bundle its own tty7-updater, because an image without one would
install fine and then be the last version that ever could.

release.yml and nightly.yml now build the updater on the Linux leg and
bundle it into the AppImage, and both check the packaged image for the
same facts the updater checks on a user's machine — helper present,
version stamped — so a packaging mistake fails the workflow instead of
the update. The first release carrying this can only bootstrap: images
already installed predate the helper and keep the manual hint, so the
first complete in-app update is the release after it.
2026-08-16 17:43:52 +08:00

113 lines
4.9 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# Usage: bundle-appimage.sh <target-triple> <arch-label>
# Package the release binary into a self-contained AppImage:
# dist/tty7-<version>-linux-<arch>.AppImage
#
# Unlike the bare tarball (bundle-linux.sh), this bundles the x11/wayland/xkb/
# fontconfig/freetype runtime libraries alongside the binary, so it launches on
# distros that don't ship the exact same set Ubuntu does — Fedora, Arch, etc.
# (glibc itself is NOT bundled — an AppImage still needs the host glibc to be
# >= the build machine's, so the release runner's Ubuntu sets the floor.)
#
# Completion signatures are loaded at runtime relative to the executable
# (<exe-dir>/completions — see terminal::signature), so they go beside the
# binary at usr/bin/completions inside the AppDir.
set -euo pipefail
TARGET="$1"
ARCH="$2"
# Anchored on `= "` — see the note in bundle-macos.sh: the root manifest leads
# with `version.workspace = true`, which a bare `^version` match would return
# verbatim as the "version" and bake into every asset filename.
VERSION="$(grep -m1 '^version = "' Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/')"
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
echo "bundle-appimage: could not read a version from Cargo.toml (got '$VERSION')" >&2
exit 1
fi
NAME="tty7-${VERSION}-linux-${ARCH}"
# AppImage tools need FUSE to self-mount; CI runners usually lack it, so extract
# and run instead. Harmless on machines that do have FUSE.
export APPIMAGE_EXTRACT_AND_RUN=1
# linuxdeploy is published per-arch; map Rust's arch label to its naming.
case "$ARCH" in
x86_64) LD_ARCH=x86_64 ;;
arm64 | aarch64) LD_ARCH=aarch64 ;;
*) echo "unsupported arch for AppImage: $ARCH" >&2; exit 1 ;;
esac
TOOLS="$(mktemp -d)"
LINUXDEPLOY="$TOOLS/linuxdeploy-${LD_ARCH}.AppImage"
APPIMAGETOOL="$TOOLS/appimagetool-${LD_ARCH}.AppImage"
curl -fsSL -o "$LINUXDEPLOY" \
"https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-${LD_ARCH}.AppImage"
curl -fsSL -o "$APPIMAGETOOL" \
"https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${LD_ARCH}.AppImage"
chmod +x "$LINUXDEPLOY" "$APPIMAGETOOL"
# NB: don't wipe dist/ — the tarball step (bundle-linux.sh) runs first and its
# artifact must survive. Only clean our own AppDir.
APPDIR="dist/AppDir"
rm -rf "$APPDIR"
mkdir -p "$APPDIR/usr/bin"
cp "target/${TARGET}/release/tty7-app" "$APPDIR/usr/bin/tty7-app"
chmod +x "$APPDIR/usr/bin/tty7-app"
# The CLI, beside the GUI as everywhere else. Unlike the tarball, an AppImage is
# mounted at a fresh /tmp/.mount_XXXX per run, so `core::cli_install` must copy
# this onto PATH rather than symlink it — a link into the mount dies the moment
# the app exits. That branch keys off $APPIMAGE, which the runtime sets.
cp "target/${TARGET}/release/tty7" "$APPDIR/usr/bin/tty7"
chmod +x "$APPDIR/usr/bin/tty7"
# The in-app updater, beside the GUI the way every platform ships it. The GUI
# copies it out of the mount into its staging directory before use — the mount
# is gone by the time an install runs (see src/bin/tty7-updater.rs).
cp "target/${TARGET}/release/tty7-updater" "$APPDIR/usr/bin/tty7-updater"
chmod +x "$APPDIR/usr/bin/tty7-updater"
# A desktop entry + icon are mandatory AppImage metadata; linuxdeploy places
# them and generates AppRun. Icon basename must match the desktop's Icon= key.
cat > "$TOOLS/tty7.desktop" <<'DESKTOP'
[Desktop Entry]
Type=Application
Name=tty7
Comment=A fast, native terminal
Exec=tty7-app
Icon=tty7
Categories=System;TerminalEmulator;
Terminal=false
StartupWMClass=tty7
DESKTOP
# The release version, stamped where the in-app updater can read it back with
# one `--appimage-extract` and no mount: a downloaded image must state the
# version it claims before it may replace the installed one (`verify_update`
# in src/bin/tty7-updater.rs). X-AppImage-Version is the AppImage convention
# for exactly this. Appended outside the heredoc, which is quoted on purpose.
echo "X-AppImage-Version=${VERSION}" >> "$TOOLS/tty7.desktop"
# linuxdeploy only accepts fixed icon resolutions (…256, 384, 512 — NOT the
# source's 1024), so downscale to 256×256.
convert assets/app-icon.png -resize 256x256 "$TOOLS/tty7.png"
# Phase 1 — populate the AppDir: copy in dependent libs (ldd + patchelf) and
# install the desktop/icon into their standard locations.
"$LINUXDEPLOY" \
--appdir "$APPDIR" \
--executable "$APPDIR/usr/bin/tty7-app" \
--desktop-file "$TOOLS/tty7.desktop" \
--icon-file "$TOOLS/tty7.png"
# Runtime-loaded completion specs live beside the binary (not bundled by
# linuxdeploy, which only tracks ELF deps), so drop them in after populate.
mkdir -p "$APPDIR/usr/bin/completions"
cp assets/completions/*.json "$APPDIR/usr/bin/completions/"
# Phase 2 — pack the finished AppDir. Done separately from linuxdeploy so the
# completions added above are included.
"$APPIMAGETOOL" "$APPDIR" "dist/${NAME}.AppImage"
chmod +x "dist/${NAME}.AppImage"
rm -rf "$APPDIR" "$TOOLS"
echo "✅ dist/${NAME}.AppImage"