Update AppImage deps and fix packaging

This commit is contained in:
deepend committed 2026-10-28 01:56:31 -06:00
1 parent 6e5ed9270e
commit 1c0c5e163b
10 files changed
+459 -141

No files matched your search

+62 -140
View File
@@ -1,6 +1,9 @@
name: AppImage Build name: AppImage Build
on: on:
workflow_dispatch:
schedule:
- cron: '23 6 * * 1'
push: push:
branches: [master, 'release/**'] branches: [master, 'release/**']
pull_request: pull_request:
@@ -29,16 +32,29 @@ jobs:
build-essential pkg-config meson ninja-build cmake \ build-essential pkg-config meson ninja-build cmake \
gettext \ gettext \
libcanberra-dev libglib2.0-dev \ libcanberra-dev libglib2.0-dev \
libarchive-dev \ libarchive-dev libsecret-1-dev \
libgtk-3-dev libfontconfig1-dev libpango1.0-dev xvfb xauth dbus-daemon at-spi2-core \ libgtk-3-dev libfontconfig1-dev libpango1.0-dev xvfb xauth dbus-daemon at-spi2-core \
libwayland-client0 libwayland-cursor0 libwayland-egl1 \ libwayland-client0 libwayland-cursor0 libwayland-egl1 \
libxkbcommon0 \ libxkbcommon0 \
libgtk-3-bin libglib2.0-bin glib-networking shared-mime-info gsettings-desktop-schemas \ libgtk-3-bin libglib2.0-bin glib-networking shared-mime-info gsettings-desktop-schemas \
liblua5.4-dev libpci-dev libperl-dev libssl-dev libayatana-appindicator3-dev \ liblua5.4-dev libpci-dev libperl-dev libssl-dev libayatana-appindicator3-dev \
perl python3 python3-minimal python3-dev python3-cffi python3-sphinx mono-devel desktop-file-utils \ perl python3 python3-sphinx mono-devel desktop-file-utils \
libbz2-dev libffi-dev libgdbm-dev liblzma-dev libncurses-dev \
libreadline-dev libsqlite3-dev libzstd-dev tk-dev uuid-dev zlib1g-dev \
openssl ca-certificates \
breeze-gtk-theme \ breeze-gtk-theme \
patchelf file curl patchelf file curl
- name: Build private Python runtime
run: |
set -eux
python_prefix="$RUNNER_TEMP/zoitechat-python"
bash packaging/appimage/build-python.sh "$python_prefix"
echo "$python_prefix/bin" >> "$GITHUB_PATH"
echo "ZOITECHAT_PYTHON_PREFIX=$python_prefix" >> "$GITHUB_ENV"
echo "LD_LIBRARY_PATH=$python_prefix/lib" >> "$GITHUB_ENV"
echo "PKG_CONFIG_PATH=$python_prefix/lib/pkgconfig" >> "$GITHUB_ENV"
- name: Configure - name: Configure
run: | run: |
set -eux set -eux
@@ -46,6 +62,7 @@ jobs:
meson setup build \ meson setup build \
--prefix=/usr \ --prefix=/usr \
-Dinstall-docs=true \ -Dinstall-docs=true \
-Ddocs-python=/usr/bin/python3 \
-Dtext-frontend=true \ -Dtext-frontend=true \
-Dwith-perl=perl \ -Dwith-perl=perl \
-Dwith-python=python3 \ -Dwith-python=python3 \
@@ -70,25 +87,8 @@ jobs:
run: | run: |
set -eux set -eux
python3_version="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" "$ZOITECHAT_PYTHON_PREFIX/bin/python3" packaging/appimage/bundle-python.py AppDir
install -Dm755 /usr/bin/perl AppDir/usr/bin/perl
install -Dm755 "$(command -v python3)" "AppDir/usr/bin/python3"
if [ -x "/usr/bin/perl" ]; then
install -Dm755 /usr/bin/perl AppDir/usr/bin/perl
fi
install -d AppDir/usr/lib
cp -a "/usr/lib/python${python3_version}" "AppDir/usr/lib/"
if [ -d "/usr/lib/python3/dist-packages" ]; then
install -d AppDir/usr/lib/python3
cp -a /usr/lib/python3/dist-packages AppDir/usr/lib/python3/
fi
if [ -d "/usr/lib/x86_64-linux-gnu/python3/dist-packages" ]; then
install -d AppDir/usr/lib/x86_64-linux-gnu/python3
cp -a /usr/lib/x86_64-linux-gnu/python3/dist-packages AppDir/usr/lib/x86_64-linux-gnu/python3/
fi
if [ -d "/usr/lib/x86_64-linux-gnu/perl-base" ]; then if [ -d "/usr/lib/x86_64-linux-gnu/perl-base" ]; then
install -d AppDir/usr/lib/x86_64-linux-gnu install -d AppDir/usr/lib/x86_64-linux-gnu
@@ -118,11 +118,6 @@ jobs:
perl -MFile::Spec -e 'print "Build host File::Spec: $INC{\"File/Spec.pm\"}\n"' perl -MFile::Spec -e 'print "Build host File::Spec: $INC{\"File/Spec.pm\"}\n"'
find AppDir/usr -path '*/File/Spec.pm' -print -quit | grep -q . find AppDir/usr -path '*/File/Spec.pm' -print -quit | grep -q .
if compgen -G '/usr/lib/x86_64-linux-gnu/libpython3*.so*' > /dev/null; then
install -d AppDir/usr/lib/x86_64-linux-gnu
cp -a /usr/lib/x86_64-linux-gnu/libpython3*.so* AppDir/usr/lib/x86_64-linux-gnu/
fi
# Every package ships the pinned font, including portable builds. # Every package ships the pinned font, including portable builds.
cmp data/fonts/NotoColorEmoji.ttf AppDir/usr/share/fonts/zoitechat/NotoColorEmoji.ttf cmp data/fonts/NotoColorEmoji.ttf AppDir/usr/share/fonts/zoitechat/NotoColorEmoji.ttf
@@ -174,126 +169,20 @@ jobs:
chmod +x linuxdeploy-x86_64.AppImage chmod +x linuxdeploy-x86_64.AppImage
curl -fL --retry 3 -o linuxdeploy-plugin-gtk \ curl -fL --retry 3 -o linuxdeploy-plugin-gtk \
https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/7a3fbc31a9e5075073ff8790f26effbac5f84453/linuxdeploy-plugin-gtk.sh
# The generated GTK hook otherwise overrides both Wayland selection
# and the user's explicit GDK_BACKEND before our AppRun is reached.
grep -Fx 'export GDK_BACKEND=x11 # Crash with Wayland backend on Wayland' linuxdeploy-plugin-gtk
sed -i '/^export GDK_BACKEND=x11 # Crash with Wayland backend on Wayland$/d' linuxdeploy-plugin-gtk
chmod +x linuxdeploy-plugin-gtk chmod +x linuxdeploy-plugin-gtk
export PATH="${PWD}:${PATH}" export PATH="${PWD}:${PATH}"
install -Dm644 /etc/ssl/certs/ca-certificates.crt \ install -Dm644 /etc/ssl/certs/ca-certificates.crt \
AppDir/etc/ssl/certs/ca-certificates.crt AppDir/etc/ssl/certs/ca-certificates.crt
cat > AppRun <<'EOF' install -m755 packaging/appimage/AppRun AppRun
#!/bin/sh
set -eu
APPDIR="${APPDIR:-$(dirname "$(readlink -f "$0")")}" export VERSION="$(cat VERSION)"
export PATH="$APPDIR/usr/bin:${PATH:-/usr/bin:/bin}"
export LD_LIBRARY_PATH="$APPDIR/usr/lib:$APPDIR/usr/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH:-}"
export XDG_DATA_DIRS="$APPDIR/usr/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
export GTK_EXE_PREFIX="$APPDIR/usr"
export GTK_DATA_PREFIX="$APPDIR/usr"
gtk_path_entries=""
if [ -d "$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0" ]; then
gtk_path_entries="$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0"
fi
if [ -d "$APPDIR/usr/lib/gtk-3.0" ]; then
gtk_path_entries="${gtk_path_entries:+$gtk_path_entries:}$APPDIR/usr/lib/gtk-3.0"
fi
if [ -n "$gtk_path_entries" ]; then
export GTK_PATH="$gtk_path_entries${GTK_PATH:+:$GTK_PATH}"
fi
if [ -d "$APPDIR/usr/lib/x86_64-linux-gnu/zoitechat/plugins" ]; then
export ZOITECHAT_LIBDIR="$APPDIR/usr/lib/x86_64-linux-gnu/zoitechat/plugins"
elif [ -d "$APPDIR/usr/lib/zoitechat/plugins" ]; then
export ZOITECHAT_LIBDIR="$APPDIR/usr/lib/zoitechat/plugins"
fi
if [ -d "$APPDIR/usr/share/glib-2.0/schemas" ]; then
export GSETTINGS_SCHEMA_DIR="$APPDIR/usr/share/glib-2.0/schemas${GSETTINGS_SCHEMA_DIR:+:$GSETTINGS_SCHEMA_DIR}"
fi
if [ -d "$APPDIR/usr/lib/x86_64-linux-gnu/gio/modules" ]; then
gio_module_dir="$APPDIR/usr/lib/x86_64-linux-gnu/gio/modules"
elif [ -d "$APPDIR/usr/lib/gio/modules" ]; then
gio_module_dir="$APPDIR/usr/lib/gio/modules"
else
gio_module_dir=""
fi
if [ -n "$gio_module_dir" ]; then
export GIO_MODULE_DIR="$gio_module_dir"
export GIO_EXTRA_MODULES="$gio_module_dir"
fi
unset GTK_MODULES
perl5lib_entries=""
for dir in \
"$APPDIR/usr/lib/x86_64-linux-gnu/perl-base" \
"$APPDIR/usr/lib/x86_64-linux-gnu/perl"/* \
"$APPDIR/usr/lib/x86_64-linux-gnu/perl5"/* \
"$APPDIR/usr/share/perl"/* \
"$APPDIR/usr/share/perl5"
do
if [ -d "$dir" ]; then
perl5lib_entries="${perl5lib_entries:+$perl5lib_entries:}$dir"
fi
done
if [ -n "$perl5lib_entries" ]; then
export PERL5LIB="$perl5lib_entries${PERL5LIB:+:$PERL5LIB}"
fi
export PYTHONHOME="$APPDIR/usr"
python_stdlib_dir="$(find "$APPDIR/usr/lib" -maxdepth 1 -type d -name 'python3.*' | head -n 1 || true)"
pythonpath_entries=""
if [ -n "$python_stdlib_dir" ] && [ -d "$python_stdlib_dir/dist-packages" ]; then
pythonpath_entries="$python_stdlib_dir/dist-packages"
fi
if [ -d "$APPDIR/usr/lib/python3/dist-packages" ]; then
pythonpath_entries="${pythonpath_entries:+$pythonpath_entries:}$APPDIR/usr/lib/python3/dist-packages"
fi
if [ -d "$APPDIR/usr/lib/x86_64-linux-gnu/python3/dist-packages" ]; then
pythonpath_entries="${pythonpath_entries:+$pythonpath_entries:}$APPDIR/usr/lib/x86_64-linux-gnu/python3/dist-packages"
fi
if [ -n "$pythonpath_entries" ]; then
export PYTHONPATH="$pythonpath_entries${PYTHONPATH:+:$PYTHONPATH}"
fi
# Register the private font without hiding the host's normal fonts.
export ZOITECHAT_EMOJI_FONT="${ZOITECHAT_EMOJI_FONT:-$APPDIR/usr/share/fonts/zoitechat/NotoColorEmoji.ttf}"
# OpenSSL trust store override
export SSL_CERT_FILE="${SSL_CERT_FILE:-$APPDIR/etc/ssl/certs/ca-certificates.crt}"
export SSL_CERT_DIR="${SSL_CERT_DIR:-$APPDIR/etc/ssl/certs}"
export CURL_CA_BUNDLE="${CURL_CA_BUNDLE:-$SSL_CERT_FILE}"
# Prefer Wayland if the session provides it, but keep X11 fallback.
# Don't override if the user already set GDK_BACKEND explicitly.
if [ -z "${GDK_BACKEND:-}" ]; then
if [ -n "${WAYLAND_DISPLAY:-}" ] || [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then
export GDK_BACKEND="wayland,x11"
else
export GDK_BACKEND="x11"
fi
fi
for hook_dir in "$APPDIR/apprun-hooks" "$APPDIR/usr/apprun-hooks"; do
if [ -d "$hook_dir" ]; then
for hook in "$hook_dir"/*.sh; do
if [ -f "$hook" ]; then
. "$hook"
fi
done
fi
done
exec "$APPDIR/usr/bin/zoitechat" "$@"
EOF
chmod +x AppRun
VERSION="$(git describe --tags --always)"
./linuxdeploy-x86_64.AppImage \ ./linuxdeploy-x86_64.AppImage \
--appdir AppDir \ --appdir AppDir \
@@ -304,7 +193,39 @@ jobs:
--output appimage --output appimage
appimage_path="$(ls -1 *.AppImage | grep -v linuxdeploy | head -n 1)" appimage_path="$(ls -1 *.AppImage | grep -v linuxdeploy | head -n 1)"
mv "$appimage_path" "Zoitechat-${VERSION}-x86_64.AppImage" output_name="Zoitechat-${VERSION}-x86_64.AppImage"
if [ "$appimage_path" != "$output_name" ]; then
mv -- "$appimage_path" "$output_name"
fi
- name: Test packaged runtime and plugins
run: |
set -eux
appimage_path="$(find . -maxdepth 1 -name 'Zoitechat-*-x86_64.AppImage' -print -quit)"
test -n "$appimage_path"
mkdir -p "$RUNNER_TEMP/appimage check"
(cd "$RUNNER_TEMP/appimage check" && "$GITHUB_WORKSPACE/$appimage_path" --appimage-extract)
# Hide the build interpreter so it cannot mask broken relocation.
mv "$ZOITECHAT_PYTHON_PREFIX" "$ZOITECHAT_PYTHON_PREFIX.hidden"
trap 'mv "$ZOITECHAT_PYTHON_PREFIX.hidden" "$ZOITECHAT_PYTHON_PREFIX"' EXIT
env -u LD_LIBRARY_PATH -u PYTHONHOME -u PYTHONPATH \
/usr/bin/python3 packaging/appimage/check-runtime.py "$RUNNER_TEMP/appimage check/squashfs-root"
- name: Record dependency versions and checksum
run: |
set -eux
mkdir -p appimage-audit
dpkg-query -W -f='${binary:Package}\t${Version}\n' > appimage-audit/build-packages.tsv
cp "$RUNNER_TEMP/appimage check/runtime.json" appimage-audit/runtime.json
sha256sum linuxdeploy-x86_64.AppImage linuxdeploy-plugin-gtk > appimage-audit/build-tools.sha256
sha256sum Zoitechat-*-x86_64.AppImage > appimage-audit/SHA256SUMS
- name: Upload dependency audit
uses: actions/upload-artifact@v7
with:
name: zoitechat-appimage-audit
path: appimage-audit/
if-no-files-found: error
- name: Attest AppImage (Build Provenance) - name: Attest AppImage (Build Provenance)
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
@@ -317,3 +238,4 @@ jobs:
with: with:
name: zoitechat-appimage name: zoitechat-appimage
path: Zoitechat-*-x86_64.AppImage path: Zoitechat-*-x86_64.AppImage
if-no-files-found: error
+1 -1
View File
@@ -184,7 +184,7 @@ endif
add_project_link_arguments(global_ldflags, language: 'c') add_project_link_arguments(global_ldflags, language: 'c')
if get_option('install-docs') if get_option('install-docs')
docs_python = find_program('python3', 'python') docs_python = find_program(get_option('docs-python'), 'python')
docs_sphinx = run_command(docs_python, '-c', 'import sphinx', check: false) docs_sphinx = run_command(docs_python, '-c', 'import sphinx', check: false)
if docs_sphinx.returncode() != 0 if docs_sphinx.returncode() != 0
error('Sphinx is required to build the local HTML documentation. Install Sphinx or configure with -Dinstall-docs=false.') error('Sphinx is required to build the local HTML documentation. Install Sphinx or configure with -Dinstall-docs=false.')
+3
View File
@@ -43,6 +43,9 @@ option('install-emoji-font', type: 'boolean', value: true,
option('install-docs', type: 'boolean', value: false, option('install-docs', type: 'boolean', value: false,
description: 'Build and install the local HTML documentation' description: 'Build and install the local HTML documentation'
) )
option('docs-python', type: 'string', value: 'python3',
description: 'Python interpreter with Sphinx for building documentation'
)
# Plugins # Plugins
option('with-checksum', type: 'boolean', option('with-checksum', type: 'boolean',
+89
View File
@@ -0,0 +1,89 @@
#!/bin/sh
set -eu
APPDIR="${APPDIR:-$(dirname "$(readlink -f "$0")")}"
export PATH="$APPDIR/usr/bin:${PATH:-/usr/bin:/bin}"
export LD_LIBRARY_PATH="$APPDIR/usr/lib:$APPDIR/usr/lib/x86_64-linux-gnu${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export XDG_DATA_DIRS="$APPDIR/usr/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
export GTK_EXE_PREFIX="$APPDIR/usr"
export GTK_DATA_PREFIX="$APPDIR/usr"
gtk_path_entries=""
if [ -d "$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0" ]; then
gtk_path_entries="$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0"
fi
if [ -d "$APPDIR/usr/lib/gtk-3.0" ]; then
gtk_path_entries="${gtk_path_entries:+$gtk_path_entries:}$APPDIR/usr/lib/gtk-3.0"
fi
if [ -n "$gtk_path_entries" ]; then
export GTK_PATH="$gtk_path_entries${GTK_PATH:+:$GTK_PATH}"
fi
if [ -d "$APPDIR/usr/lib/x86_64-linux-gnu/zoitechat/plugins" ]; then
export ZOITECHAT_LIBDIR="$APPDIR/usr/lib/x86_64-linux-gnu/zoitechat/plugins"
elif [ -d "$APPDIR/usr/lib/zoitechat/plugins" ]; then
export ZOITECHAT_LIBDIR="$APPDIR/usr/lib/zoitechat/plugins"
fi
if [ -d "$APPDIR/usr/share/glib-2.0/schemas" ]; then
export GSETTINGS_SCHEMA_DIR="$APPDIR/usr/share/glib-2.0/schemas"
fi
if [ -d "$APPDIR/usr/lib/x86_64-linux-gnu/gio/modules" ]; then
gio_module_dir="$APPDIR/usr/lib/x86_64-linux-gnu/gio/modules"
elif [ -d "$APPDIR/usr/lib/gio/modules" ]; then
gio_module_dir="$APPDIR/usr/lib/gio/modules"
else
gio_module_dir=""
fi
if [ -n "$gio_module_dir" ]; then
export GIO_MODULE_DIR="$gio_module_dir"
export GIO_EXTRA_MODULES="$gio_module_dir"
fi
unset GTK_MODULES
perl5lib_entries=""
for dir in \
"$APPDIR/usr/lib/x86_64-linux-gnu/perl-base" \
"$APPDIR/usr/lib/x86_64-linux-gnu/perl"/* \
"$APPDIR/usr/lib/x86_64-linux-gnu/perl5"/* \
"$APPDIR/usr/share/perl"/* \
"$APPDIR/usr/share/perl5"
do
if [ -d "$dir" ]; then
perl5lib_entries="${perl5lib_entries:+$perl5lib_entries:}$dir"
fi
done
if [ -n "$perl5lib_entries" ]; then
export PERL5LIB="$perl5lib_entries${PERL5LIB:+:$PERL5LIB}"
fi
export PYTHONHOME="$APPDIR/usr"
# CPython finds the private versioned site-packages directory itself.
export TCL_LIBRARY="${TCL_LIBRARY:-$APPDIR/usr/share/tcltk/tcl8.6}"
export TK_LIBRARY="${TK_LIBRARY:-$APPDIR/usr/share/tcltk/tk8.6}"
# Register the private font without hiding the host's normal fonts.
export ZOITECHAT_EMOJI_FONT="${ZOITECHAT_EMOJI_FONT:-$APPDIR/usr/share/fonts/zoitechat/NotoColorEmoji.ttf}"
# OpenSSL trust store override
export SSL_CERT_FILE="${SSL_CERT_FILE:-$APPDIR/etc/ssl/certs/ca-certificates.crt}"
export SSL_CERT_DIR="${SSL_CERT_DIR:-$APPDIR/etc/ssl/certs}"
export CURL_CA_BUNDLE="${CURL_CA_BUNDLE:-$SSL_CERT_FILE}"
# Prefer Wayland if the session provides it, but keep X11 fallback.
# Don't override if the user already set GDK_BACKEND explicitly.
if [ -z "${GDK_BACKEND:-}" ]; then
if [ -n "${WAYLAND_DISPLAY:-}" ] || [ "${XDG_SESSION_TYPE:-}" = "wayland" ]; then
export GDK_BACKEND="wayland,x11"
else
export GDK_BACKEND="x11"
fi
fi
# linuxdeploy's outer AppRun has already sourced its generated hooks.
exec "$APPDIR/usr/bin/zoitechat" "$@"
+82
View File
@@ -0,0 +1,82 @@
# AppImage build
The workflow stays on Ubuntu 22.04 so updating Python does not also raise the
AppImage's glibc requirement. Python is built separately from a checked source
archive. `python.env` holds its version and SHA-256. CFFI and pycparser are pinned
in `python-requirements.txt`, including wheel hashes.
The Python interpreter used to build the documentation is separate from the
one used by the scripting plugin. The `docs-python` Meson option selects the
interpreter with Sphinx; it defaults to `python3` for normal builds.
## What was in the published build
Checked on October 27, 2026, using the 2.20.0-dev release asset:
`Zoitechat-2.20.0-dev-x86_64.AppImage`
SHA-256: `062eff9ba37556d3eca6c17388db45451353732a0cdf15c2bf3d6a3eae620989`
| Component | Published AppImage | This build |
| --- | --- | --- |
| Python | 3.10.12, built August 31, 2026 | 3.14.10 |
| CFFI | 1.15.0 | 2.1.1 |
| pycparser | 2.21 | 3.12 |
| Perl | 5.34.0 | Ubuntu 22.04 package, with available updates |
| OpenSSL reported by Python | 3.0.2 | Ubuntu 22.04 package, with available updates |
The old build copied the runner's entire Python `dist-packages` tree. That
included cloud-init, the Azure agent, apt bindings, Sphinx and other build-host
packages. The new bundle includes the standard library and the two declared
Python plugin dependencies. It leaves out CPython regression tests, bytecode
caches, static libraries and build tools. Tcl/Tk's runtime scripts are included
for the standard library's tkinter module.
Python 3.10 reached upstream end of life in October 2026. That does not mean
every Ubuntu Python 3.10 package is unpatched: Ubuntu backports security fixes.
The same applies to the old-looking OpenSSL and Perl version strings. The old
AppImage did not record the Ubuntu package revisions, so those strings alone
cannot establish its vulnerability status.
Python 3.14 is a deliberate choice. It is maintained upstream and supported by
CFFI. Moving a scripting runtime can still affect scripts using removed Python
APIs or native extensions built for Python 3.10. Third-party modules required
by user scripts need to be supplied for the new interpreter; incidental runner
packages are no longer treated as part of the scripting API.
## Checks and rebuilds
The workflow can be run manually and rebuilds weekly to pick up available
Ubuntu package and CA certificate updates. It creates an artifact; releasing a
new download is still a separate step. Already downloaded AppImages do not
receive those updates.
After packaging, the workflow extracts the AppImage into a path containing a
space and hides the Python build prefix. `check-runtime.py` checks the bundled
interpreter, native standard-library extensions, CFFI, CA loading and GIO TLS.
It starts the actual application under Xvfb and runs Python, Perl and Lua
scripts through their plugins. A missing script marker, process error or timeout
fails the build. Existing emoji tests remain in the workflow.
The `zoitechat-appimage-audit` artifact contains runtime versions, the build
host's exact Ubuntu package revisions, hashes of the deployment helpers and
the final AppImage checksum. The package list describes the build host, not an
SBOM of the AppImage. linuxdeploy's `continuous` download and its output plugin
remain upstream-managed; their versions should be reviewed when packaging
changes.
The GTK deployment helper is pinned to a commit. Its unconditional X11
override is removed so AppRun can honour `GDK_BACKEND` and choose Wayland with
X11 fallback. Hooks run once through linuxdeploy's generated outer launcher.
The Xvfb test covers X11; Wayland still needs testing in a Wayland session.
When updating Python, change its version and checksum together, refresh the
two wheel pins and hashes, and run the workflow. Review the audit artifact and
test the result on both Ubuntu 22.04 and a current distribution before release.
References:
- [Python release status](https://devguide.python.org/versions/)
- [Python 3.14.10](https://www.python.org/downloads/release/python-31410/)
- [CFFI release notes](https://cffi.readthedocs.io/en/latest/whatsnew.html)
- [Ubuntu security updates](https://documentation.ubuntu.com/security/security-updates/)
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd -- "$(dirname -- "$0")" && pwd)"
# shellcheck source=python.env
source "$script_dir/python.env"
prefix="${1:?Usage: build-python.sh ABSOLUTE_INSTALL_PREFIX}"
[[ "$prefix" = /* ]]
work_dir="$(mktemp -d)"
trap 'rm -rf "$work_dir"' EXIT
curl -fL --retry 3 -o "$work_dir/python.tar.xz" \
"https://www.python.org/ftp/python/$PYTHON_VERSION/Python-$PYTHON_VERSION.tar.xz"
printf '%s %s\n' "$PYTHON_SHA256" "$work_dir/python.tar.xz" | sha256sum --check --strict
tar -xJf "$work_dir/python.tar.xz" -C "$work_dir"
cd "$work_dir/Python-$PYTHON_VERSION"
# Build on the AppImage's oldest supported distribution. A newer runner
# would raise the glibc requirement for the interpreter and its extensions.
./configure --prefix="$prefix" --enable-shared --without-static-libpython --with-ensurepip=install
make -j"$(nproc)"
LD_LIBRARY_PATH="$PWD${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" make install
export LD_LIBRARY_PATH="$prefix/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
"$prefix/bin/python3" -m pip install --disable-pip-version-check \
--only-binary=:all: --require-hashes -r "$script_dir/python-requirements.txt"
# Fail here if configure silently omitted a standard-library extension.
"$prefix/bin/python3" -I -c \
'import bz2, ctypes, curses, dbm.gnu, lzma, readline, sqlite3, ssl, tkinter, zlib, cffi, _cffi_backend; print(ssl.OPENSSL_VERSION)'
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
"""Copy the private interpreter, stdlib and declared plugin dependencies."""
import importlib.metadata
from pathlib import Path
import shutil
import sys
import sysconfig
def main():
destination = Path(sys.argv[1]).resolve() / "usr"
version = f"{sys.version_info.major}.{sys.version_info.minor}"
stdlib = Path(sysconfig.get_path("stdlib"))
target_stdlib = destination / "lib" / f"python{version}"
target_stdlib.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(
stdlib, target_stdlib,
ignore=shutil.ignore_patterns("site-packages", "__pycache__", "*.a", "test", "tests"),
)
(destination / "bin").mkdir(parents=True, exist_ok=True)
shutil.copy2(sys.executable, destination / "bin" / "python3")
libdir = Path(sysconfig.get_config_var("LIBDIR"))
library = sysconfig.get_config_var("LDLIBRARY")
for source in libdir.glob(f"{library}*"):
# Dereference the build prefix's symlinks so none point outside AppDir.
shutil.copy2(source, destination / "lib" / source.name)
site = target_stdlib / "site-packages"
site.mkdir()
for name in ("cffi", "pycparser"):
distribution = importlib.metadata.distribution(name)
for entry in distribution.files:
# Wheel metadata and the native backend belong to the runtime;
# scripts installed outside site-packages belong to the build.
if ".." in entry.parts or entry.suffix == ".pyc":
continue
source = Path(distribution.locate_file(entry))
target = site / entry
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, target)
# _tkinter also needs Tcl/Tk's scripts, not just its ELF dependencies.
for name in ("tcl8.6", "tk8.6"):
shutil.copytree(Path("/usr/share/tcltk") / name,
destination / "share/tcltk" / name)
if __name__ == "__main__":
main()
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""Exercise an extracted AppImage without borrowing the build interpreter."""
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
PROBE = r'''
import bz2, ctypes, curses, dbm.gnu, json, lzma, pathlib, readline, sqlite3, ssl, sys, tkinter, zlib
import cffi, _cffi_backend, pycparser
root = pathlib.Path(sys.argv[1]).resolve()
assert pathlib.Path(sys.base_prefix).resolve() == root / 'usr', sys.base_prefix
for module in (cffi, _cffi_backend, pycparser, ssl, sqlite3):
assert pathlib.Path(module.__file__).resolve().is_relative_to(root), module.__file__
assert cffi.FFI().sizeof('int') > 0
assert tkinter.Tcl().eval('info patchlevel').startswith('8.6.')
assert ssl.create_default_context().get_ca_certs(), 'No trusted CA certificates'
gio = ctypes.CDLL('libgio-2.0.so.0')
gio.g_tls_backend_get_default.restype = ctypes.c_void_p
gio.g_tls_backend_supports_tls.argtypes = [ctypes.c_void_p]
assert gio.g_tls_backend_supports_tls(gio.g_tls_backend_get_default()), 'GIO TLS backend unavailable'
print(json.dumps({'python': sys.version, 'openssl': ssl.OPENSSL_VERSION,
'cffi': cffi.__version__, 'pycparser': pycparser.__version__}))
'''
PYTHON_PLUGIN = r'''
import os
from pathlib import Path
import sys
import zoitechat, hexchat, xchat
__module_name__ = 'AppImage Python check'
__module_version__ = '1'
root = Path(os.environ['ZOITECHAT_SMOKE'])
(root / 'python.ok').write_text(sys.version)
def finish(userdata):
if all((root / (name + '.ok')).is_file() for name in ('python', 'perl', 'lua')):
# Return from Python before shutdown unloads the native plugin.
zoitechat.command('TIMER 0.1 KILLALL')
return False
return True
zoitechat.hook_timer(100, finish)
'''
PERL_PLUGIN = r'''
use strict;
use warnings;
use File::Spec;
use Encode;
ZoiteChat::register('AppImage Perl check', '1', 'Runtime check');
open my $marker, '>', File::Spec->catfile($ENV{ZOITECHAT_SMOKE}, 'perl.ok') or die $!;
print {$marker} $^V;
close $marker or die $!;
'''
LUA_PLUGIN = r'''
zoitechat.register('AppImage Lua check', '1', 'Runtime check')
local marker = assert(io.open(os.getenv('ZOITECHAT_SMOKE') .. '/lua.ok', 'w'))
marker:write(_VERSION)
marker:close()
'''
def run(command, env):
try:
result = subprocess.run(command, env=env, text=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, timeout=90)
except subprocess.TimeoutExpired as error:
raise RuntimeError(f"{command[0]} timed out:\n{error.stdout}\n{error.stderr}") from error
if result.returncode:
raise RuntimeError(f"{command[0]} failed:\n{result.stdout}\n{result.stderr}")
return result.stdout.strip()
def main():
appdir = Path(sys.argv[1]).resolve()
env = os.environ.copy()
for key in ("PYTHONHOME", "PYTHONPATH", "PERL5LIB", "LD_LIBRARY_PATH",
"GIO_MODULE_DIR", "GIO_EXTRA_MODULES"):
env.pop(key, None)
env.update({
"APPDIR": str(appdir),
"PYTHONHOME": str(appdir / "usr"),
"PYTHONNOUSERSITE": "1",
"LD_LIBRARY_PATH": f"{appdir}/usr/lib:{appdir}/usr/lib/x86_64-linux-gnu",
"SSL_CERT_FILE": str(appdir / "etc/ssl/certs/ca-certificates.crt"),
"SSL_CERT_DIR": str(appdir / "etc/ssl/certs"),
"GDK_BACKEND": "x11",
"TCL_LIBRARY": str(appdir / "usr/share/tcltk/tcl8.6"),
"TK_LIBRARY": str(appdir / "usr/share/tcltk/tk8.6"),
})
for relative in ("usr/lib/x86_64-linux-gnu/gio/modules", "usr/lib/gio/modules"):
if (appdir / relative).is_dir():
env["GIO_MODULE_DIR"] = str(appdir / relative)
env["GIO_EXTRA_MODULES"] = str(appdir / relative)
break
perl_dirs = [appdir / "usr/lib/x86_64-linux-gnu/perl-base"]
for relative in ("usr/lib/x86_64-linux-gnu/perl", "usr/lib/x86_64-linux-gnu/perl5", "usr/share/perl"):
perl_dirs.extend(sorted((appdir / relative).glob("*")))
perl_dirs.append(appdir / "usr/share/perl5")
env["PERL5LIB"] = ":".join(str(path) for path in perl_dirs if path.is_dir())
runtime = json.loads(run([str(appdir / "usr/bin/python3"), "-s", "-c", PROBE, str(appdir)], env))
expected = dict(line.split("=", 1) for line in
Path(__file__).with_name("python.env").read_text().splitlines()
if line and not line.startswith("#"))["PYTHON_VERSION"]
assert runtime["python"].split()[0] == expected, runtime
runtime["perl"] = run([str(appdir / "usr/bin/perl"), "-MFile::Spec", "-MEncode", "-MPOSIX",
"-MJSON::PP", "-e", "print $^V"], env)
with tempfile.TemporaryDirectory(prefix="zoitechat-smoke-") as temporary:
config = Path(temporary)
(config / "zoitechat.conf").write_text("gui_slist_skip = 1\n")
autoload = config / "addons"
autoload.mkdir()
for filename, content in (("smoke.py", PYTHON_PLUGIN), ("smoke.pl", PERL_PLUGIN), ("smoke.lua", LUA_PLUGIN)):
(autoload / filename).write_text(content)
env["HOME"] = temporary
env["XDG_CONFIG_HOME"] = temporary
env["ZOITECHAT_SMOKE"] = temporary
output = run(["xvfb-run", "-a", "dbus-run-session", "--", str(appdir / "AppRun"),
"--run-as-root", "--no-auto", "--cfgdir", temporary], env)
for name in ("python", "perl", "lua"):
assert (config / f"{name}.ok").is_file(), f"{name} plugin failed:\n{output}"
runtime["lua"] = (config / "lua.ok").read_text()
(appdir.parent / "runtime.json").write_text(json.dumps(runtime, indent=2) + "\n")
print(json.dumps(runtime, indent=2))
if __name__ == "__main__":
main()
@@ -0,0 +1,5 @@
# CPython 3.14, Linux x86_64. Hashes are for the PyPI wheels.
cffi==2.1.1 \
--hash=sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96
pycparser==3.12 \
--hash=sha256:a076f3f489b39b43ed13563a1dafe42447445bec2e294154ec5c57fcdda4c85e
+4
View File
@@ -0,0 +1,4 @@
# Update the version and upstream source checksum together.
# https://www.python.org/downloads/release/python-31410/
PYTHON_VERSION=3.14.10
PYTHON_SHA256=de414a57119abef52784d8993e341d53aed635fc2b05c2607d9f98a2c5b72032