mirror of
https://github.com/ZoiteChat/zoitechat.git
synced 2026-10-28 14:15:38 +00:00
Update AppImage deps and fix packaging
This commit is contained in:
10 files changed
+459
-141
No files matched your search
@@ -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
@@ -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.')
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Executable
+89
@@ -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" "$@"
|
||||||
@@ -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/)
|
||||||
Executable
+29
@@ -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)'
|
||||||
Executable
+50
@@ -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()
|
||||||
Executable
+134
@@ -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
|
||||||
@@ -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
|
||||||
Reference in new issue
Block a user