From a4df408e6e934c9cef911613ad4e545c7df3c8ea Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 10 Nov 2026 19:14:38 -0600 Subject: [PATCH 1/7] linux: set the X11 window icon from the bundle The runner never set a window icon, so X11 taskbars and alt-tab only showed one when a desktop file happened to match WM_CLASS. Install the app's icon.png as data/app_icon.png and load it at startup. GTK3 caps _NET_WM_ICON at 262144 longs and silently drops anything larger. A single 512x512 image needs 262146, so pass 16 to 256 px copies instead. The same limit means gtk_window_set_icon_name() with only a 512x512 themed icon also sets nothing. --- ...sktop => com.cypherstack.campfire.desktop} | 0 ...sktop => com.cypherstack.stackduo.desktop} | 0 ...op => com.cypherstack.stackwallet.desktop} | 0 .../app_config/templates/linux/CMakeLists.txt | 5 +++ .../templates/linux/my_application.cc | 33 +++++++++++++++++++ 5 files changed, 38 insertions(+) rename appimage/campfire/{campfire.desktop => com.cypherstack.campfire.desktop} (100%) rename appimage/stack_duo/{stack_duo.desktop => com.cypherstack.stackduo.desktop} (100%) rename appimage/stack_wallet/{stack_wallet.desktop => com.cypherstack.stackwallet.desktop} (100%) diff --git a/appimage/campfire/campfire.desktop b/appimage/campfire/com.cypherstack.campfire.desktop similarity index 100% rename from appimage/campfire/campfire.desktop rename to appimage/campfire/com.cypherstack.campfire.desktop diff --git a/appimage/stack_duo/stack_duo.desktop b/appimage/stack_duo/com.cypherstack.stackduo.desktop similarity index 100% rename from appimage/stack_duo/stack_duo.desktop rename to appimage/stack_duo/com.cypherstack.stackduo.desktop diff --git a/appimage/stack_wallet/stack_wallet.desktop b/appimage/stack_wallet/com.cypherstack.stackwallet.desktop similarity index 100% rename from appimage/stack_wallet/stack_wallet.desktop rename to appimage/stack_wallet/com.cypherstack.stackwallet.desktop diff --git a/scripts/app_config/templates/linux/CMakeLists.txt b/scripts/app_config/templates/linux/CMakeLists.txt index 1eca6af115..a746cbc967 100644 --- a/scripts/app_config/templates/linux/CMakeLists.txt +++ b/scripts/app_config/templates/linux/CMakeLists.txt @@ -152,6 +152,11 @@ install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR} install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) +# X11 window icon, loaded by my_application.cc. +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../asset_sources/icon/${BINARY_NAME}/icon.png" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" RENAME "app_icon.png" + COMPONENT Runtime) + if(USE_SYSTEM_SECURE_STORAGE_DEPS) file(GLOB JSONCPP_SO_FILES "${JSONCPP_LIBDIR}/libjsoncpp.so*") install(FILES ${JSONCPP_SO_FILES} DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" diff --git a/scripts/app_config/templates/linux/my_application.cc b/scripts/app_config/templates/linux/my_application.cc index 0954bbc891..7ff84743b7 100644 --- a/scripts/app_config/templates/linux/my_application.cc +++ b/scripts/app_config/templates/linux/my_application.cc @@ -17,6 +17,38 @@ static void first_frame_cb(MyApplication* self, FlView *view) gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); } +// X11 window icon from the PNG that CMake installs into data/. Wayland +// ignores this and takes the icon from the .desktop file matching +// APPLICATION_ID. GTK drops icon lists over 262144 longs, and a single +// 512x512 image is already over, so pass scaled copies. +static void set_window_icon(GtkWindow* window) { + g_autofree gchar* exe = g_file_read_link("/proc/self/exe", nullptr); + if (exe == nullptr) { + return; + } + g_autofree gchar* dir = g_path_get_dirname(exe); + g_autofree gchar* path = + g_build_filename(dir, "data", "app_icon.png", nullptr); + g_autoptr(GError) error = nullptr; + g_autoptr(GdkPixbuf) source = gdk_pixbuf_new_from_file(path, &error); + if (source == nullptr) { + g_debug("No window icon: %s", error->message); + return; + } + + static const int sizes[] = {16, 24, 32, 48, 64, 128, 256}; + GList* icons = nullptr; + for (int size : sizes) { + GdkPixbuf* icon = + gdk_pixbuf_scale_simple(source, size, size, GDK_INTERP_BILINEAR); + if (icon != nullptr) { + icons = g_list_append(icons, icon); + } + } + gtk_window_set_icon_list(window, icons); + g_list_free_full(icons, g_object_unref); +} + // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); @@ -37,6 +69,7 @@ static void my_application_activate(GApplication* application) { gtk_window_set_title(window, "PlaceHolderName"); } + set_window_icon(window); gtk_window_set_default_size(window, 1220, 500); gtk_widget_show(GTK_WIDGET(window)); From 5a1e0e3e663956289c84bf637f67b451fd80cd13 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 10 Nov 2026 19:14:38 -0600 Subject: [PATCH 2/7] flatpak: link metainfo to its desktop file Add so appstream tooling can tie the component to the desktop file and its icon. --- flatpak/com.cypherstack.campfire.metainfo.xml | 1 + flatpak/com.cypherstack.stackduo.metainfo.xml | 1 + flatpak/com.cypherstack.stackwallet.metainfo.xml | 1 + 3 files changed, 3 insertions(+) diff --git a/flatpak/com.cypherstack.campfire.metainfo.xml b/flatpak/com.cypherstack.campfire.metainfo.xml index f355750b40..16c6409728 100644 --- a/flatpak/com.cypherstack.campfire.metainfo.xml +++ b/flatpak/com.cypherstack.campfire.metainfo.xml @@ -12,5 +12,6 @@ https://campfireprivacy.com https://github.com/cypherstack/stack_wallet/issues + com.cypherstack.campfire.desktop diff --git a/flatpak/com.cypherstack.stackduo.metainfo.xml b/flatpak/com.cypherstack.stackduo.metainfo.xml index 1e929dec2b..af32d8dc80 100644 --- a/flatpak/com.cypherstack.stackduo.metainfo.xml +++ b/flatpak/com.cypherstack.stackduo.metainfo.xml @@ -12,5 +12,6 @@ https://stackwallet.com https://github.com/cypherstack/stack_wallet/issues + com.cypherstack.stackduo.desktop diff --git a/flatpak/com.cypherstack.stackwallet.metainfo.xml b/flatpak/com.cypherstack.stackwallet.metainfo.xml index abf3b19c93..ba6f0dde23 100644 --- a/flatpak/com.cypherstack.stackwallet.metainfo.xml +++ b/flatpak/com.cypherstack.stackwallet.metainfo.xml @@ -13,5 +13,6 @@ https://stackwallet.com https://github.com/cypherstack/stack_wallet/issues + com.cypherstack.stackwallet.desktop From 93712b14c2fb6d0d8b6ca84bf96f862278acdfaf Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 10 Nov 2026 19:14:38 -0600 Subject: [PATCH 3/7] appimage: name the desktop entry after the app ID and ship icons The running window reports com.cypherstack. as its app ID and WM_CLASS, but the AppImage desktop entry was .desktop with Icon=, so nothing matched it to the window, even after Gear Lever or AppImageLauncher installed it. Name the entry and icon after the app ID and add StartupWMClass, which still matches when integrators rename the file. appimage/make_appdir.sh now builds the AppDir for all three CI jobs: root icon plus .DirIcon, usr/share/icons/hicolor, usr/share/applications, and the flatpak metainfo as usr/share/metainfo/.appdata.xml, which appimagetool validates with appstreamcli when it is installed. --- .github/workflows/build.yaml | 21 ++------ .../campfire/com.cypherstack.campfire.desktop | 3 +- appimage/make_appdir.sh | 52 +++++++++++++++++++ .../com.cypherstack.stackduo.desktop | 3 +- .../com.cypherstack.stackwallet.desktop | 3 +- 5 files changed, 61 insertions(+), 21 deletions(-) create mode 100755 appimage/make_appdir.sh diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a2695a91ba..060f832ef1 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -664,12 +664,7 @@ jobs: run: | VERSION="${{ steps.ver.outputs.version }}" tar -xzf "stack_wallet-linux-x86_64-${VERSION}.tar.gz" - mkdir -p AppDir - cp -r bundle/* AppDir/ - cp appimage/stack_wallet/AppRun AppDir/AppRun - chmod +x AppDir/AppRun - cp appimage/stack_wallet/stack_wallet.desktop AppDir/ - cp asset_sources/icon/stack_wallet/icon.png AppDir/stack_wallet.png + appimage/make_appdir.sh stack_wallet bundle AppDir curl -fsSL -o appimagetool \ https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage chmod +x appimagetool @@ -1122,12 +1117,7 @@ jobs: run: | VERSION="${{ steps.ver.outputs.version }}" tar -xzf "campfire-linux-x86_64-${VERSION}.tar.gz" - mkdir -p AppDir - cp -r bundle/* AppDir/ - cp appimage/campfire/AppRun AppDir/AppRun - chmod +x AppDir/AppRun - cp appimage/campfire/campfire.desktop AppDir/ - cp asset_sources/icon/campfire/icon.png AppDir/campfire.png + appimage/make_appdir.sh campfire bundle AppDir curl -fsSL -o appimagetool \ https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage chmod +x appimagetool @@ -1582,12 +1572,7 @@ jobs: run: | VERSION="${{ steps.ver.outputs.version }}" tar -xzf "stack_duo-linux-x86_64-${VERSION}.tar.gz" - mkdir -p AppDir - cp -r bundle/* AppDir/ - cp appimage/stack_duo/AppRun AppDir/AppRun - chmod +x AppDir/AppRun - cp appimage/stack_duo/stack_duo.desktop AppDir/ - cp asset_sources/icon/stack_duo/icon.png AppDir/stack_duo.png + appimage/make_appdir.sh stack_duo bundle AppDir curl -fsSL -o appimagetool \ https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage chmod +x appimagetool diff --git a/appimage/campfire/com.cypherstack.campfire.desktop b/appimage/campfire/com.cypherstack.campfire.desktop index 71b7b612c0..911bdec976 100644 --- a/appimage/campfire/com.cypherstack.campfire.desktop +++ b/appimage/campfire/com.cypherstack.campfire.desktop @@ -2,6 +2,7 @@ Name=Campfire Comment=Your privacy. Your wallet. Your Firo. Exec=campfire -Icon=campfire +Icon=com.cypherstack.campfire +StartupWMClass=com.cypherstack.campfire Type=Application Categories=Finance; diff --git a/appimage/make_appdir.sh b/appimage/make_appdir.sh new file mode 100755 index 0000000000..c5880c1c2e --- /dev/null +++ b/appimage/make_appdir.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Assemble an AppDir from a built Linux bundle. +# Usage: appimage/make_appdir.sh +set -euo pipefail + +if [ $# -ne 3 ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +app=$1 +bundle=$2 +appdir=$3 + +case "$app" in + stack_wallet) id=com.cypherstack.stackwallet ;; + stack_duo) id=com.cypherstack.stackduo ;; + campfire) id=com.cypherstack.campfire ;; + *) echo "unknown app: $app" >&2; exit 1 ;; +esac + +root=$(cd "$(dirname "$0")/.." && pwd) +icon="$root/asset_sources/icon/$app/icon.png" + +# hicolor directories are named after the icon size, so read it from the +# PNG header (width and height are big-endian at bytes 16 and 20). +width=$((16#$(od -An -j16 -N4 -tx1 "$icon" | tr -d ' \n'))) +height=$((16#$(od -An -j20 -N4 -tx1 "$icon" | tr -d ' \n'))) +if [ "$width" -ne "$height" ]; then + echo "$icon is ${width}x${height}, expected a square icon" >&2 + exit 1 +fi + +mkdir -p "$appdir" +cp -r "$bundle"/. "$appdir"/ +install -m755 "$root/appimage/$app/AppRun" "$appdir/AppRun" +install -m644 "$root/appimage/$app/$id.desktop" "$appdir/$id.desktop" + +# appimagetool embeds the icon named by Icon= from the AppDir root. File +# managers and integrators (Gear Lever, AppImageLauncher) read .DirIcon. +install -m644 "$icon" "$appdir/$id.png" +ln -sfn "$id.png" "$appdir/.DirIcon" + +# Standard AppDir locations, also read by integrators. +install -Dm644 "$icon" \ + "$appdir/usr/share/icons/hicolor/${width}x${width}/apps/$id.png" +install -Dm644 "$root/appimage/$app/$id.desktop" \ + "$appdir/usr/share/applications/$id.desktop" +# appimagetool validates this with appstreamcli when it is installed; the +# must resolve to the desktop file above. +install -Dm644 "$root/flatpak/$id.metainfo.xml" \ + "$appdir/usr/share/metainfo/$id.appdata.xml" diff --git a/appimage/stack_duo/com.cypherstack.stackduo.desktop b/appimage/stack_duo/com.cypherstack.stackduo.desktop index 64b9f6de44..76160ee5ce 100644 --- a/appimage/stack_duo/com.cypherstack.stackduo.desktop +++ b/appimage/stack_duo/com.cypherstack.stackduo.desktop @@ -2,6 +2,7 @@ Name=Stack Duo Comment=An open-source, multicoin wallet for everyone Exec=stack_duo -Icon=stack_duo +Icon=com.cypherstack.stackduo +StartupWMClass=com.cypherstack.stackduo Type=Application Categories=Finance; diff --git a/appimage/stack_wallet/com.cypherstack.stackwallet.desktop b/appimage/stack_wallet/com.cypherstack.stackwallet.desktop index 19c6fcce56..fb5032664b 100644 --- a/appimage/stack_wallet/com.cypherstack.stackwallet.desktop +++ b/appimage/stack_wallet/com.cypherstack.stackwallet.desktop @@ -2,6 +2,7 @@ Name=Stack Wallet Comment=Open-source non-custodial cryptocurrency wallet Exec=stack_wallet -Icon=stack_wallet +Icon=com.cypherstack.stackwallet +StartupWMClass=com.cypherstack.stackwallet Type=Application Categories=Finance; From 8156adc4313fc95377073a3f6a45bd4ad0e487fb Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 10 Nov 2026 19:17:22 -0600 Subject: [PATCH 4/7] ci: build AppImages with AppImage/appimagetool AppImageKit is archived. The maintained appimagetool embeds the static type2 runtime, so the AppImages no longer need libfuse2 on the user's system. --- .github/workflows/build.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 060f832ef1..2488cbb80d 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -666,7 +666,7 @@ jobs: tar -xzf "stack_wallet-linux-x86_64-${VERSION}.tar.gz" appimage/make_appdir.sh stack_wallet bundle AppDir curl -fsSL -o appimagetool \ - https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage + https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage chmod +x appimagetool ARCH=x86_64 ./appimagetool --appimage-extract-and-run AppDir \ "stack_wallet-appimage-x86_64-${VERSION}.AppImage" @@ -1119,7 +1119,7 @@ jobs: tar -xzf "campfire-linux-x86_64-${VERSION}.tar.gz" appimage/make_appdir.sh campfire bundle AppDir curl -fsSL -o appimagetool \ - https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage + https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage chmod +x appimagetool ARCH=x86_64 ./appimagetool --appimage-extract-and-run AppDir \ "campfire-appimage-x86_64-${VERSION}.AppImage" @@ -1574,7 +1574,7 @@ jobs: tar -xzf "stack_duo-linux-x86_64-${VERSION}.tar.gz" appimage/make_appdir.sh stack_duo bundle AppDir curl -fsSL -o appimagetool \ - https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage + https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage chmod +x appimagetool ARCH=x86_64 ./appimagetool --appimage-extract-and-run AppDir \ "stack_duo-appimage-x86_64-${VERSION}.AppImage" From 9a0ea85760197307eedc778458ad1c81886efea8 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 10 Nov 2026 20:59:38 -0600 Subject: [PATCH 5/7] appimage: don't ship metainfo in the AppDir appimagetool validates usr/share/metainfo with appstreamcli, which fetches every . campfireprivacy.com no longer resolves, so the Campfire AppImage build failed, and any site being down would fail the others. Flatpak builds keep the metainfo; flatpak-builder does not fetch URLs. --- appimage/make_appdir.sh | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/appimage/make_appdir.sh b/appimage/make_appdir.sh index c5880c1c2e..10f0197dc2 100755 --- a/appimage/make_appdir.sh +++ b/appimage/make_appdir.sh @@ -46,7 +46,5 @@ install -Dm644 "$icon" \ "$appdir/usr/share/icons/hicolor/${width}x${width}/apps/$id.png" install -Dm644 "$root/appimage/$app/$id.desktop" \ "$appdir/usr/share/applications/$id.desktop" -# appimagetool validates this with appstreamcli when it is installed; the -# must resolve to the desktop file above. -install -Dm644 "$root/flatpak/$id.metainfo.xml" \ - "$appdir/usr/share/metainfo/$id.appdata.xml" +# No usr/share/metainfo: appimagetool would validate it with appstreamcli, +# which fetches every and fails the build when one is unreachable. From b660397838adf25f0e29c209dada77213f3cea13 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 12 Nov 2026 19:24:04 -0600 Subject: [PATCH 6/7] build: pin AppImage release toolchain --- .github/workflows/build.yaml | 18 +---- appimage/build_appimage.sh | 84 +++++++++++++++++++++ scripts/linux/tests/test_appimage.py | 109 +++++++++++++++++++++++++++ 3 files changed, 196 insertions(+), 15 deletions(-) create mode 100755 appimage/build_appimage.sh create mode 100644 scripts/linux/tests/test_appimage.py diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2488cbb80d..4d393a690b 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -664,11 +664,7 @@ jobs: run: | VERSION="${{ steps.ver.outputs.version }}" tar -xzf "stack_wallet-linux-x86_64-${VERSION}.tar.gz" - appimage/make_appdir.sh stack_wallet bundle AppDir - curl -fsSL -o appimagetool \ - https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage - chmod +x appimagetool - ARCH=x86_64 ./appimagetool --appimage-extract-and-run AppDir \ + appimage/build_appimage.sh stack_wallet bundle \ "stack_wallet-appimage-x86_64-${VERSION}.AppImage" - uses: actions/upload-artifact@v7 @@ -1117,11 +1113,7 @@ jobs: run: | VERSION="${{ steps.ver.outputs.version }}" tar -xzf "campfire-linux-x86_64-${VERSION}.tar.gz" - appimage/make_appdir.sh campfire bundle AppDir - curl -fsSL -o appimagetool \ - https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage - chmod +x appimagetool - ARCH=x86_64 ./appimagetool --appimage-extract-and-run AppDir \ + appimage/build_appimage.sh campfire bundle \ "campfire-appimage-x86_64-${VERSION}.AppImage" - uses: actions/upload-artifact@v7 @@ -1572,11 +1564,7 @@ jobs: run: | VERSION="${{ steps.ver.outputs.version }}" tar -xzf "stack_duo-linux-x86_64-${VERSION}.tar.gz" - appimage/make_appdir.sh stack_duo bundle AppDir - curl -fsSL -o appimagetool \ - https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage - chmod +x appimagetool - ARCH=x86_64 ./appimagetool --appimage-extract-and-run AppDir \ + appimage/build_appimage.sh stack_duo bundle \ "stack_duo-appimage-x86_64-${VERSION}.AppImage" - uses: actions/upload-artifact@v7 diff --git a/appimage/build_appimage.sh b/appimage/build_appimage.sh new file mode 100755 index 0000000000..003e513984 --- /dev/null +++ b/appimage/build_appimage.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Build an x86_64 AppImage with pinned, hash-verified AppImage tooling. +# Usage: appimage/build_appimage.sh +set -euo pipefail + +readonly APPIMAGETOOL_VERSION="1.9.1" +readonly APPIMAGETOOL_SHA256="ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0" +readonly APPIMAGE_RUNTIME_VERSION="20251108" +readonly APPIMAGE_RUNTIME_SHA256="2fca8b443c92510f1483a883f60061ad09b46b978b2631c807cd873a47ec260d" +APPIMAGE_WORKDIR="" + +verify_sha256() { + local file=$1 + local expected=$2 + local actual + actual=$(sha256sum "$file" | cut -d ' ' -f1) + if [ "$actual" != "$expected" ]; then + echo "SHA-256 mismatch for $file: expected $expected, got $actual" >&2 + return 1 + fi +} + +download_verified() { + local url=$1 + local expected=$2 + local destination=$3 + curl --fail --silent --show-error --location --retry 3 \ + --proto '=https' --proto-redir '=https' \ + --output "$destination" "$url" + verify_sha256 "$destination" "$expected" +} + +main() { + if [ $# -ne 3 ]; then + echo "usage: $0 " >&2 + return 1 + fi + + local app=$1 + local bundle=$2 + local output=$3 + case "$app" in + stack_wallet|stack_duo|campfire) ;; + *) echo "unknown app: $app" >&2; return 1 ;; + esac + if [ ! -d "$bundle" ]; then + echo "bundle directory not found: $bundle" >&2 + return 1 + fi + + local root + root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) + local output_dir + output_dir=$(dirname -- "$output") + mkdir -p "$output_dir" + output_dir=$(cd -- "$output_dir" && pwd) + output="$output_dir/$(basename -- "$output")" + + local build_root="$root/build/appimage-work" + mkdir -p "$build_root" + APPIMAGE_WORKDIR=$(mktemp -d "$build_root/run.XXXXXX") + trap 'rm -rf -- "$APPIMAGE_WORKDIR"' EXIT + + local appdir="$APPIMAGE_WORKDIR/AppDir" + local appimagetool="$APPIMAGE_WORKDIR/appimagetool-x86_64.AppImage" + local runtime="$APPIMAGE_WORKDIR/runtime-x86_64" + + "$root/appimage/make_appdir.sh" "$app" "$bundle" "$appdir" + download_verified \ + "https://github.com/AppImage/appimagetool/releases/download/${APPIMAGETOOL_VERSION}/appimagetool-x86_64.AppImage" \ + "$APPIMAGETOOL_SHA256" "$appimagetool" + download_verified \ + "https://github.com/AppImage/type2-runtime/releases/download/${APPIMAGE_RUNTIME_VERSION}/runtime-x86_64" \ + "$APPIMAGE_RUNTIME_SHA256" "$runtime" + chmod +x "$appimagetool" + + ARCH=x86_64 "$appimagetool" --appimage-extract-and-run \ + --no-appstream --runtime-file "$runtime" \ + "$appdir" "$output" +} + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + main "$@" +fi diff --git a/scripts/linux/tests/test_appimage.py b/scripts/linux/tests/test_appimage.py new file mode 100644 index 0000000000..15b5f679bd --- /dev/null +++ b/scripts/linux/tests/test_appimage.py @@ -0,0 +1,109 @@ +"""Offline regression tests for AppImage layout and pinned release tooling.""" + +import hashlib +from pathlib import Path +import subprocess +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[3] +MAKE_APPDIR = ROOT / "appimage" / "make_appdir.sh" +BUILD_APPIMAGE = ROOT / "appimage" / "build_appimage.sh" +BUILD_WORKFLOW = ROOT / ".github" / "workflows" / "build.yaml" + +APP_IDS = { + "stack_wallet": "com.cypherstack.stackwallet", + "stack_duo": "com.cypherstack.stackduo", + "campfire": "com.cypherstack.campfire", +} + + +class AppImageScriptsTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory(prefix="appimage-test-") + self.addCleanup(self.temp.cleanup) + self.temp_path = Path(self.temp.name) + self.bundle = self.temp_path / "bundle" + self.bundle.mkdir() + (self.bundle / "bundle-marker").write_text("copied\n") + + def test_appdir_layout_for_all_apps(self): + for app, app_id in APP_IDS.items(): + with self.subTest(app=app): + appdir = self.temp_path / f"{app} AppDir" + subprocess.run( + [str(MAKE_APPDIR), app, str(self.bundle), str(appdir)], + check=True, + capture_output=True, + text=True, + ) + self.assertTrue((appdir / "AppRun").stat().st_mode & 0o111) + self.assertTrue((appdir / "bundle-marker").is_file()) + self.assertTrue((appdir / f"{app_id}.desktop").is_file()) + self.assertTrue((appdir / f"{app_id}.png").is_file()) + self.assertEqual((appdir / ".DirIcon").readlink(), Path(f"{app_id}.png")) + self.assertTrue( + (appdir / "usr/share/applications" / f"{app_id}.desktop").is_file() + ) + self.assertTrue( + ( + appdir + / "usr/share/icons/hicolor/512x512/apps" + / f"{app_id}.png" + ).is_file() + ) + self.assertFalse((appdir / "usr/share/metainfo").exists()) + + def test_unknown_app_is_rejected(self): + result = subprocess.run( + [str(MAKE_APPDIR), "unknown", str(self.bundle), str(self.temp_path / "out")], + capture_output=True, + text=True, + ) + self.assertNotEqual(result.returncode, 0) + self.assertIn("unknown app", result.stderr) + + def test_release_toolchain_is_immutable_and_explicit(self): + script = BUILD_APPIMAGE.read_text() + self.assertNotIn("/continuous/", script) + self.assertIn("releases/download/${APPIMAGETOOL_VERSION}", script) + self.assertIn("releases/download/${APPIMAGE_RUNTIME_VERSION}", script) + self.assertIn("--proto '=https'", script) + self.assertIn("--proto-redir '=https'", script) + self.assertIn("--runtime-file", script) + self.assertIn("verify_sha256", script) + self.assertRegex(script, r'APPIMAGETOOL_SHA256="[0-9a-f]{64}"') + self.assertRegex(script, r'APPIMAGE_RUNTIME_SHA256="[0-9a-f]{64}"') + + def test_workflow_uses_one_shared_packaging_entrypoint(self): + workflow = BUILD_WORKFLOW.read_text() + self.assertNotIn("AppImage/appimagetool/releases/download/continuous", workflow) + self.assertEqual(workflow.count("appimage/build_appimage.sh"), 3) + for app in APP_IDS: + self.assertIn(f"appimage/build_appimage.sh {app} bundle", workflow) + + def test_checksum_verification_accepts_match_and_rejects_mismatch(self): + payload = self.temp_path / "payload" + payload.write_bytes(b"known AppImage tool fixture\n") + digest = hashlib.sha256(payload.read_bytes()).hexdigest() + command = 'source "$1"; verify_sha256 "$2" "$3"' + + valid = subprocess.run( + ["bash", "-c", command, "bash", str(BUILD_APPIMAGE), str(payload), digest], + capture_output=True, + text=True, + ) + self.assertEqual(valid.returncode, 0, valid.stdout + valid.stderr) + + invalid = subprocess.run( + ["bash", "-c", command, "bash", str(BUILD_APPIMAGE), str(payload), "0" * 64], + capture_output=True, + text=True, + ) + self.assertNotEqual(invalid.returncode, 0) + self.assertIn("SHA-256 mismatch", invalid.stderr) + + +if __name__ == "__main__": + unittest.main() From 8dc9d9dae014309676c57da4367ae93ae0f9f8b3 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 12 Nov 2026 19:51:09 -0600 Subject: [PATCH 7/7] chore: harden AppImage CI downloads --- .github/workflows/build.yaml | 6 ++++++ appimage/build_appimage.sh | 1 + scripts/linux/tests/test_appimage.py | 17 +++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4d393a690b..e0559aeebc 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -634,6 +634,8 @@ jobs: build-appimage: runs-on: ubuntu-24.04 needs: build-linux + permissions: + contents: read steps: - uses: actions/checkout@v6 @@ -1083,6 +1085,8 @@ jobs: build-campfire-appimage: runs-on: ubuntu-24.04 needs: build-campfire-linux + permissions: + contents: read steps: - uses: actions/checkout@v6 @@ -1534,6 +1538,8 @@ jobs: build-stack-duo-appimage: runs-on: ubuntu-24.04 needs: build-stack-duo-linux + permissions: + contents: read steps: - uses: actions/checkout@v6 diff --git a/appimage/build_appimage.sh b/appimage/build_appimage.sh index 003e513984..dd4097d1a7 100755 --- a/appimage/build_appimage.sh +++ b/appimage/build_appimage.sh @@ -25,6 +25,7 @@ download_verified() { local expected=$2 local destination=$3 curl --fail --silent --show-error --location --retry 3 \ + --connect-timeout 30 --max-time 300 \ --proto '=https' --proto-redir '=https' \ --output "$destination" "$url" verify_sha256 "$destination" "$expected" diff --git a/scripts/linux/tests/test_appimage.py b/scripts/linux/tests/test_appimage.py index 15b5f679bd..b41e94041b 100644 --- a/scripts/linux/tests/test_appimage.py +++ b/scripts/linux/tests/test_appimage.py @@ -71,6 +71,8 @@ def test_release_toolchain_is_immutable_and_explicit(self): self.assertIn("releases/download/${APPIMAGE_RUNTIME_VERSION}", script) self.assertIn("--proto '=https'", script) self.assertIn("--proto-redir '=https'", script) + self.assertIn("--connect-timeout 30", script) + self.assertIn("--max-time 300", script) self.assertIn("--runtime-file", script) self.assertIn("verify_sha256", script) self.assertRegex(script, r'APPIMAGETOOL_SHA256="[0-9a-f]{64}"') @@ -80,6 +82,21 @@ def test_workflow_uses_one_shared_packaging_entrypoint(self): workflow = BUILD_WORKFLOW.read_text() self.assertNotIn("AppImage/appimagetool/releases/download/continuous", workflow) self.assertEqual(workflow.count("appimage/build_appimage.sh"), 3) + self.assertEqual(workflow.count("build-appimage:\n"), 1) + self.assertEqual(workflow.count("build-campfire-appimage:\n"), 1) + self.assertEqual(workflow.count("build-stack-duo-appimage:\n"), 1) + for job, needs in { + "build-appimage": "build-linux", + "build-campfire-appimage": "build-campfire-linux", + "build-stack-duo-appimage": "build-stack-duo-linux", + }.items(): + self.assertIn( + f" {job}:\n" + " runs-on: ubuntu-24.04\n" + f" needs: {needs}\n" + " permissions:\n contents: read\n steps:\n", + workflow, + ) for app in APP_IDS: self.assertIn(f"appimage/build_appimage.sh {app} bundle", workflow)