From 07c47c308a679bce9bc1531fb85b21c13238e2db Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Mon, 9 Feb 2026 13:27:50 +0100 Subject: [PATCH] feat: bundle shared libraries in portable Linux tarball --- .github/workflows/build.yml | 50 +++++++++-- linux/packaging/bundle-libs.sh | 155 +++++++++++++++++++++++++++++++++ linux/packaging/plezy.sh | 48 +++++++++- 3 files changed, 242 insertions(+), 11 deletions(-) create mode 100755 linux/packaging/bundle-libs.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 05a15771..7ae1e5a0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -446,14 +446,31 @@ jobs: - name: Build Linux x64 run: flutter build linux --release --dart-define=ENABLE_UPDATE_CHECK=true + - name: Build Linux Packages + run: ARCH_SUFFIX=x64 OUTPUT_DIR=$GITHUB_WORKSPACE python3 linux/packaging/build-packages.py + + - name: Bundle shared libraries for portable tarball + run: bash linux/packaging/bundle-libs.sh build/linux/x64/release/bundle + + - name: Copy wrapper script into bundle + run: cp linux/packaging/plezy.sh build/linux/x64/release/bundle/plezy.sh + + - name: Verify no missing dependencies + run: | + cd build/linux/x64/release/bundle + MISSING=$(LD_LIBRARY_PATH=lib ldd ./plezy 2>&1 | grep "not found" || true) + if [[ -n "$MISSING" ]]; then + echo "ERROR: Unresolved dependencies found:" >&2 + echo "$MISSING" >&2 + exit 1 + fi + echo "All dependencies resolved." + - name: Create tarball run: | cd build/linux/x64/release/bundle tar -czf $GITHUB_WORKSPACE/plezy-linux-x64.tar.gz * - - name: Build Linux Packages - run: ARCH_SUFFIX=x64 OUTPUT_DIR=$GITHUB_WORKSPACE python3 linux/packaging/build-packages.py - - name: Attest Linux x64 artifacts uses: actions/attest-build-provenance@v2 with: @@ -523,11 +540,6 @@ jobs: BUNDLE_DIR=$(find build/linux -path "*/release/bundle" -type d | head -1) echo "bundle_dir=$BUNDLE_DIR" >> $GITHUB_OUTPUT - - name: Create tarball - run: | - cd ${{ steps.find-bundle.outputs.bundle_dir }} - tar -czf $GITHUB_WORKSPACE/plezy-linux-arm64.tar.gz * - - name: Build Linux Packages run: | BUILD_DIR=${{ steps.find-bundle.outputs.bundle_dir }} \ @@ -535,6 +547,28 @@ jobs: OUTPUT_DIR=$GITHUB_WORKSPACE \ python3 linux/packaging/build-packages.py + - name: Bundle shared libraries for portable tarball + run: bash linux/packaging/bundle-libs.sh ${{ steps.find-bundle.outputs.bundle_dir }} + + - name: Copy wrapper script into bundle + run: cp linux/packaging/plezy.sh ${{ steps.find-bundle.outputs.bundle_dir }}/plezy.sh + + - name: Verify no missing dependencies + run: | + cd ${{ steps.find-bundle.outputs.bundle_dir }} + MISSING=$(LD_LIBRARY_PATH=lib ldd ./plezy 2>&1 | grep "not found" || true) + if [[ -n "$MISSING" ]]; then + echo "ERROR: Unresolved dependencies found:" >&2 + echo "$MISSING" >&2 + exit 1 + fi + echo "All dependencies resolved." + + - name: Create tarball + run: | + cd ${{ steps.find-bundle.outputs.bundle_dir }} + tar -czf $GITHUB_WORKSPACE/plezy-linux-arm64.tar.gz * + - name: Attest Linux arm64 artifacts uses: actions/attest-build-provenance@v2 with: diff --git a/linux/packaging/bundle-libs.sh b/linux/packaging/bundle-libs.sh new file mode 100755 index 00000000..930f4b03 --- /dev/null +++ b/linux/packaging/bundle-libs.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +# +# bundle-libs.sh — Bundle shared libraries and GTK modules into a Flutter +# Linux bundle directory so the resulting tarball is portable. +# +# Usage: bundle-libs.sh +# +# The bundle directory should contain the main binary and a lib/ subdirectory. +# After running, lib/ will contain all required shared libraries (minus the +# exclusion list), GTK/GDK modules, and GSettings schemas. + +set -euo pipefail + +BUNDLE_DIR="$(realpath "$1")" +LIB_DIR="$BUNDLE_DIR/lib" +BINARY="$BUNDLE_DIR/plezy" + +if [[ ! -f "$BINARY" ]]; then + echo "Error: binary not found at $BINARY" >&2 + exit 1 +fi + +# ── Exclusion list ────────────────────────────────────────────────────────── +# Libraries that must come from the host system. +EXCLUDE_PATTERN='( + linux-vdso\.so + |ld-linux.*\.so + |libc\.so + |libm\.so + |libpthread\.so + |libdl\.so + |librt\.so + |libmvec\.so + |libresolv\.so + |libnss_.*\.so + |libGL\.so + |libEGL\.so + |libGLX\.so + |libGLESv2\.so + |libGLdispatch\.so + |libOpenGL\.so + |libvulkan\.so + |libdrm\.so + |libgbm\.so + |libnvidia.*\.so + |libcuda.*\.so + |libasound\.so +)' + +# ── Helper: collect ldd deps ─────────────────────────────────────────────── +collect_deps() { + # Collect resolved library paths from ldd output for the given files. + # Filters out excluded libs and libs that don't exist on disk. + ldd "$@" 2>/dev/null \ + | grep -oP '\s(/\S+\.so[^\s]*)' \ + | sort -u \ + | grep -vE "$EXCLUDE_PATTERN" \ + || true +} + +echo "==> Discovering shared library dependencies..." + +# Seed: the main binary + all .so files already in lib/ +mapfile -t seed_files < <(find "$LIB_DIR" -name '*.so*' -type f 2>/dev/null; echo "$BINARY") + +# Iteratively resolve until no new deps appear +declare -A seen +while true; do + mapfile -t deps < <(collect_deps "${seed_files[@]}") + new_files=() + for dep in "${deps[@]}"; do + base="$(basename "$dep")" + if [[ -z "${seen[$base]:-}" && ! -f "$LIB_DIR/$base" ]]; then + seen[$base]=1 + cp -L "$dep" "$LIB_DIR/$base" 2>/dev/null || true + new_files+=("$LIB_DIR/$base") + fi + done + if [[ ${#new_files[@]} -eq 0 ]]; then + break + fi + echo " Bundled ${#new_files[@]} new libraries" + seed_files=("${new_files[@]}") +done + +# ── GTK modules (loaded via dlopen, invisible to ldd) ────────────────────── + +bundle_module_dir() { + local src_dir="$1" dest_dir="$2" + if [[ -d "$src_dir" ]]; then + echo "==> Bundling modules from $src_dir" + mkdir -p "$dest_dir" + find "$src_dir" -name '*.so' -exec cp -L {} "$dest_dir/" \; + # Resolve transitive deps of module .so files + mapfile -t mod_deps < <(collect_deps "$dest_dir"/*.so 2>/dev/null) + for dep in "${mod_deps[@]}"; do + base="$(basename "$dep")" + if [[ ! -f "$LIB_DIR/$base" ]]; then + cp -L "$dep" "$LIB_DIR/$base" 2>/dev/null || true + fi + done + fi +} + +# gdk-pixbuf loaders +PIXBUF_MODULE_DIR="$(pkg-config --variable=gdk_pixbuf_moduledir gdk-pixbuf-2.0 2>/dev/null || true)" +PIXBUF_DEST="$LIB_DIR/gdk-pixbuf-2.0/2.10.0/loaders" +if [[ -n "$PIXBUF_MODULE_DIR" && -d "$PIXBUF_MODULE_DIR" ]]; then + bundle_module_dir "$PIXBUF_MODULE_DIR" "$PIXBUF_DEST" + # Regenerate loaders.cache with relative paths + GDK_PIXBUF_MODULEDIR="$PIXBUF_DEST" \ + gdk-pixbuf-query-loaders "$PIXBUF_DEST"/*.so > "$LIB_DIR/gdk-pixbuf-2.0/2.10.0/loaders.cache" 2>/dev/null || true +fi + +# GIO modules +GIO_MODULE_DIR="$(pkg-config --variable=giomoduledir gio-2.0 2>/dev/null || true)" +GIO_DEST="$LIB_DIR/gio/modules" +if [[ -n "$GIO_MODULE_DIR" && -d "$GIO_MODULE_DIR" ]]; then + bundle_module_dir "$GIO_MODULE_DIR" "$GIO_DEST" +fi + +# GTK IM modules + print backends +GTK_LIBDIR="$(pkg-config --variable=libdir gtk+-3.0 2>/dev/null || true)" +if [[ -n "$GTK_LIBDIR" ]]; then + IM_MODULE_DIR="$GTK_LIBDIR/gtk-3.0/3.0.0/immodules" + IM_DEST="$LIB_DIR/gtk-3.0/3.0.0/immodules" + bundle_module_dir "$IM_MODULE_DIR" "$IM_DEST" + + # Regenerate IM modules cache + if [[ -d "$IM_DEST" ]] && command -v gtk-query-immodules-3.0 &>/dev/null; then + gtk-query-immodules-3.0 "$IM_DEST"/*.so > "$IM_DEST/../immodules.cache" 2>/dev/null || true + fi + + PRINT_MODULE_DIR="$GTK_LIBDIR/gtk-3.0/3.0.0/printbackends" + PRINT_DEST="$LIB_DIR/gtk-3.0/3.0.0/printbackends" + bundle_module_dir "$PRINT_MODULE_DIR" "$PRINT_DEST" +fi + +# ── GSettings schemas ───────────────────────────────────────────────────── +SCHEMAS_SRC="/usr/share/glib-2.0/schemas" +SCHEMAS_DEST="$BUNDLE_DIR/share/glib-2.0/schemas" +if [[ -d "$SCHEMAS_SRC" ]]; then + echo "==> Bundling GSettings schemas" + mkdir -p "$SCHEMAS_DEST" + cp "$SCHEMAS_SRC"/*.xml "$SCHEMAS_DEST/" 2>/dev/null || true + cp "$SCHEMAS_SRC"/*.override "$SCHEMAS_DEST/" 2>/dev/null || true + glib-compile-schemas "$SCHEMAS_DEST" 2>/dev/null || true +fi + +# ── Strip debug symbols ─────────────────────────────────────────────────── +echo "==> Stripping debug symbols..." +find "$LIB_DIR" -name '*.so*' -type f -exec strip --strip-unneeded {} \; 2>/dev/null || true + +echo "==> Done. Bundled libraries in $LIB_DIR" +du -sh "$LIB_DIR" diff --git a/linux/packaging/plezy.sh b/linux/packaging/plezy.sh index 29a0bcb0..b5ef3be0 100644 --- a/linux/packaging/plezy.sh +++ b/linux/packaging/plezy.sh @@ -1,3 +1,45 @@ -#!/bin/bash -cd /opt/plezy -exec ./plezy "$@" +#!/usr/bin/env bash +# Wrapper script for Plezy. Works for both: +# - Portable tarball (run from extracted directory) +# - System packages (/usr/bin/plezy -> /opt/plezy/) + +# Resolve the real path of this script, following symlinks +SCRIPT_PATH="$(readlink -f "$0")" +INSTALL_DIR="$(dirname "$SCRIPT_PATH")" + +# If we're in /usr/bin, the actual install is in /opt/plezy +if [[ "$INSTALL_DIR" == "/usr/bin" ]]; then + INSTALL_DIR="/opt/plezy" +fi + +# Prepend bundled libraries to LD_LIBRARY_PATH +if [[ -d "$INSTALL_DIR/lib" ]]; then + export LD_LIBRARY_PATH="$INSTALL_DIR/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +fi + +# gdk-pixbuf loaders +if [[ -f "$INSTALL_DIR/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache" ]]; then + export GDK_PIXBUF_MODULE_FILE="$INSTALL_DIR/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache" +fi + +# GIO modules +if [[ -d "$INSTALL_DIR/lib/gio/modules" ]]; then + export GIO_MODULE_DIR="$INSTALL_DIR/lib/gio/modules" +fi + +# GTK IM modules +if [[ -f "$INSTALL_DIR/lib/gtk-3.0/3.0.0/immodules.cache" ]]; then + export GTK_IM_MODULE_FILE="$INSTALL_DIR/lib/gtk-3.0/3.0.0/immodules.cache" +fi + +# GTK module path (print backends, etc.) +if [[ -d "$INSTALL_DIR/lib/gtk-3.0" ]]; then + export GTK_PATH="$INSTALL_DIR/lib/gtk-3.0" +fi + +# GSettings schemas +if [[ -d "$INSTALL_DIR/share/glib-2.0/schemas" ]]; then + export GSETTINGS_SCHEMA_DIR="$INSTALL_DIR/share/glib-2.0/schemas" +fi + +exec "$INSTALL_DIR/plezy" "$@"