Skip to content

Build Linux (tar.gz + AppImage) #23

Build Linux (tar.gz + AppImage)

Build Linux (tar.gz + AppImage) #23

Workflow file for this run

name: Build Linux (tar.gz + AppImage)
on:
workflow_dispatch:
permissions:
contents: read
env:
APP_NAME: MiruShin
PUBSPEC_APP_NAME: mirushin
LINUX_APP_ID: com.emp0ry.mirushin
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
# Source libmdk from GitHub *releases* (stable, tested) — the SAME source
# the working Windows build uses. By default fvp downloads the SourceForge
# *nightly* at build time (see fvp cmake/deps.cmake), so the bundled libmdk
# silently changes day to day; a June 1 nightly regressed and crashes on
# playback (SIGSEGV at 0x0 in a libmdk FrameReader/MediaIO::createForProtocol
# worker thread). Using releases/latest avoids the unstable nightly channel.
FVP_DEPS_URL: https://github.com/wang-bin/mdk-sdk/releases/latest/download
jobs:
build-linux:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup Flutter (stable)
uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- name: Install Linux build deps
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev \
libfuse2 libglib2.0-0 libsecret-1-dev libpulse0 python3-pil
# libmdk ships its OWN FFmpeg (libffmpeg.so.8, bundled below), so the
# host does not need the system ffmpeg/VA-API stack. libpulse0 stays
# because libmdk DT_NEEDs libpulse.so.0 and linuxdeploy bundles it
# from the host.
- name: Flutter pub get
run: flutter pub get
- name: Force fresh pinned MDK SDK
run: |
# fvp's cmake/deps.cmake skips re-downloading libmdk if an mdk-sdk is
# already extracted in its package dir. Because ~/.pub-cache can be
# cached between runs, a previously-downloaded (broken nightly) libmdk
# could otherwise persist and silently ignore FVP_DEPS_URL. Delete any
# cached/extracted mdk-sdk so the pinned stable SDK is always fetched.
find "$HOME/.pub-cache" -type d -path "*fvp*/linux/mdk-sdk" -prune -exec rm -rf {} + 2>/dev/null || true
find "$HOME/.pub-cache" -type f -path "*fvp*/linux/mdk-sdk-*.tar.xz*" -delete 2>/dev/null || true
echo "Cleared cached mdk-sdk; build will fetch FVP_DEPS_URL=$FVP_DEPS_URL"
- name: Strip Linux MediaKit plugin
run: bash .github/scripts/strip-linux-media-kit.sh
- name: Build Linux (release)
run: flutter build linux --release --no-pub
- name: Read version from pubspec.yaml
id: ver
shell: bash
run: |
VERSION=$(grep '^version:' pubspec.yaml | awk '{print $2}' | cut -d'+' -f1)
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Create tar.gz bundle
id: tgz
shell: bash
run: |
set -euo pipefail
VERSION="${{ steps.ver.outputs.version }}"
OUT_DIR="build/linux/x64/release/bundle"
if [ ! -d "$OUT_DIR" ]; then
echo "ERROR: bundle not found at $OUT_DIR"
ls -la build/linux/x64/release || true
exit 1
fi
TGZ="${APP_NAME}-linux-v${VERSION}.tar.gz"
tar -C "$OUT_DIR" -czf "$TGZ" .
echo "tgz_path=$TGZ" >> "$GITHUB_OUTPUT"
- name: Build AppImage
id: appimage
shell: bash
run: |
set -euo pipefail
VERSION="${{ steps.ver.outputs.version }}"
OUT_DIR="build/linux/x64/release/bundle"
curl -L -o linuxdeploy-x86_64.AppImage \
https://github.com/linuxdeploy/linuxdeploy/releases/latest/download/linuxdeploy-x86_64.AppImage
curl -L -o linuxdeploy-plugin-gtk.sh \
https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh
chmod +x linuxdeploy-x86_64.AppImage
chmod +x linuxdeploy-plugin-gtk.sh
rm -rf AppDir
mkdir -p AppDir/usr/bin
cp -r "$OUT_DIR/"* AppDir/usr/bin/
APP_BIN="AppDir/usr/bin/${PUBSPEC_APP_NAME}"
if [ ! -x "$APP_BIN" ]; then
APP_BIN="$(find AppDir/usr/bin -maxdepth 1 -type f -executable | head -n 1)"
fi
if [ -z "$APP_BIN" ]; then
echo "ERROR: cannot find main executable in AppDir/usr/bin"
ls -la AppDir/usr/bin || true
exit 1
fi
mkdir -p AppDir/usr/share/applications
cat > "AppDir/usr/share/applications/${LINUX_APP_ID}.desktop" <<EOAPP
[Desktop Entry]
Type=Application
Name=${APP_NAME}
Exec=$(basename "$APP_BIN")
Icon=${LINUX_APP_ID}
Categories=AudioVideo;
StartupWMClass=${APP_NAME}
EOAPP
if [ -f "assets/icons/logo.png" ]; then
ICON_SOURCE="assets/icons/logo.png"
elif [ -f "linux/runner/resources/logo.png" ]; then
ICON_SOURCE="linux/runner/resources/logo.png"
else
echo "ERROR: no app logo found at assets/icons/logo.png or linux/runner/resources/logo.png"
exit 1
fi
ICON_PATH="${LINUX_APP_ID}.png"
python3 -c "from PIL import Image; Image.open('$ICON_SOURCE').resize((256, 256), Image.LANCZOS).save('$ICON_PATH')"
if [ ! -f "$ICON_PATH" ]; then
echo "ERROR: icon was not created at $ICON_PATH"
exit 1
fi
export LINUXDEPLOY=./linuxdeploy-x86_64.AppImage
export LINUXDEPLOY_PLUGIN_GTK=./linuxdeploy-plugin-gtk.sh
# Phase 1: let linuxdeploy populate the AppDir (deps, AppRun, desktop,
# icon, GTK) but DO NOT package yet (no --output).
./linuxdeploy-x86_64.AppImage \
--appdir AppDir \
--executable "$APP_BIN" \
--desktop-file "AppDir/usr/share/applications/${LINUX_APP_ID}.desktop" \
--icon-file "$ICON_PATH" \
--plugin gtk
# Phase 2: libmdk dlopens its FFmpeg backend (libffmpeg.so.*) + codec
# plugins at RUNTIME — they are NOT in libmdk's DT_NEEDED graph, so
# linuxdeploy never bundles them and playback SIGSEGVs the instant it
# starts (MediaIO::create -> NULL FFmpeg I/O vtable). Physically copy
# the whole mdk runtime into usr/lib (where linuxdeploy put libmdk) so
# dlopen finds it via the AppRun's LD_LIBRARY_PATH.
MDK_LIB_DIR="$(dirname "$APP_BIN")/lib"
mkdir -p AppDir/usr/lib
copied=0
for so in "$MDK_LIB_DIR"/libffmpeg.so* "$MDK_LIB_DIR"/libmdk*.so* "$MDK_LIB_DIR"/libc++.so*; do
[ -e "$so" ] || continue
cp -aP "$so" AppDir/usr/lib/ && echo "Bundled $(basename "$so")" && copied=1
done
if [ "$copied" != 1 ]; then
echo "ERROR: no libmdk runtime libs found in $MDK_LIB_DIR"
ls -la "$MDK_LIB_DIR" || true
exit 1
fi
# Phase 3: package the AppDir verbatim. appimagetool copies the tree
# as-is, so the dlopen-only libs we just placed cannot be stripped.
curl -L -o appimagetool-x86_64.AppImage \
https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage
FINAL="${APP_NAME}-linux-v${VERSION}.AppImage"
ARCH=x86_64 ./appimagetool-x86_64.AppImage --no-appstream AppDir "$FINAL"
chmod +x "$FINAL"
# Guard: fail the build (don't ship) if libffmpeg didn't make it in.
rm -rf squashfs-root
"./$FINAL" --appimage-extract >/dev/null 2>&1 || true
if ! ls squashfs-root/usr/lib/libffmpeg.so* >/dev/null 2>&1; then
echo "ERROR: libffmpeg.so missing from AppImage — playback would crash."
find squashfs-root -name 'libmdk*' -o -name 'libffmpeg*' || true
exit 1
fi
echo "Verified: $(ls squashfs-root/usr/lib/libffmpeg.so*) bundled in AppImage"
rm -rf squashfs-root
echo "appimage_path=$FINAL" >> "$GITHUB_OUTPUT"
- name: Build .deb package
id: deb
shell: bash
run: |
set -euo pipefail
VERSION="${{ steps.ver.outputs.version }}"
OUT_DIR="build/linux/x64/release/bundle"
PKG="${PUBSPEC_APP_NAME}"
DEB_DIR="deb/${PKG}"
# Install the whole bundle under /usr/lib/<app> so the executable's
# RPATH ($ORIGIN/lib) finds every bundled lib — including libmdk and
# its dlopen'd libffmpeg.so.8, which sit together in lib/. No
# relocation here, so nothing can get separated (unlike the AppImage).
rm -rf deb
mkdir -p "$DEB_DIR/DEBIAN" \
"$DEB_DIR/usr/lib/${PKG}" \
"$DEB_DIR/usr/bin" \
"$DEB_DIR/usr/share/applications" \
"$DEB_DIR/usr/share/icons/hicolor/256x256/apps"
cp -r "$OUT_DIR/"* "$DEB_DIR/usr/lib/${PKG}/"
ln -s "/usr/lib/${PKG}/${PKG}" "$DEB_DIR/usr/bin/${PKG}"
cat > "$DEB_DIR/usr/share/applications/${LINUX_APP_ID}.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=${APP_NAME}
Exec=${PKG}
Icon=${LINUX_APP_ID}
Categories=AudioVideo;
StartupWMClass=${APP_NAME}
EOF
# Icon: render several sizes into the hicolor theme + a pixmaps
# fallback so every desktop environment finds it.
if [ -f "assets/icons/logo.png" ]; then ICON_SRC="assets/icons/logo.png"
elif [ -f "linux/runner/resources/logo.png" ]; then ICON_SRC="linux/runner/resources/logo.png"
else echo "ERROR: no source logo for .deb icon"; exit 1; fi
mkdir -p "$DEB_DIR/usr/share/pixmaps"
for sz in 16 32 48 64 128 256 512; do
d="$DEB_DIR/usr/share/icons/hicolor/${sz}x${sz}/apps"
mkdir -p "$d"
python3 -c "from PIL import Image; Image.open('$ICON_SRC').convert('RGBA').resize(($sz,$sz), Image.LANCZOS).save('$d/${LINUX_APP_ID}.png')"
done
cp "$DEB_DIR/usr/share/icons/hicolor/256x256/apps/${LINUX_APP_ID}.png" \
"$DEB_DIR/usr/share/pixmaps/${LINUX_APP_ID}.png"
# Refresh icon + desktop caches on install/remove so the launcher
# shows the icon immediately.
cat > "$DEB_DIR/DEBIAN/postinst" <<'EOF'
#!/bin/sh
set -e
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
gtk-update-icon-cache -f -t /usr/share/icons/hicolor >/dev/null 2>&1 || true
fi
if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database -q /usr/share/applications >/dev/null 2>&1 || true
fi
exit 0
EOF
cp "$DEB_DIR/DEBIAN/postinst" "$DEB_DIR/DEBIAN/postrm"
chmod 0755 "$DEB_DIR/DEBIAN/postinst" "$DEB_DIR/DEBIAN/postrm"
INSTALLED_KB=$(du -sk "$DEB_DIR/usr" | cut -f1)
cat > "$DEB_DIR/DEBIAN/control" <<EOF
Package: ${PKG}
Version: ${VERSION}
Section: video
Priority: optional
Architecture: amd64
Maintainer: emp0ry <avagyanerik13@gmail.com>
Installed-Size: ${INSTALLED_KB}
Depends: libgtk-3-0, libglib2.0-0, libsecret-1-0, libpulse0, libgl1, libegl1
Description: ${APP_NAME} anime player
Cross-platform anime streaming/player app.
EOF
DEB="${APP_NAME}-linux-v${VERSION}.deb"
dpkg-deb --build --root-owner-group "$DEB_DIR" "$DEB"
echo "deb_path=$DEB" >> "$GITHUB_OUTPUT"
dpkg-deb -c "$DEB" | grep -E 'libffmpeg|libmdk\.so' || true
- name: Upload Linux artifacts
uses: actions/upload-artifact@v6
with:
name: MiruShin-linux
path: |
${{ steps.appimage.outputs.appimage_path }}
${{ steps.tgz.outputs.tgz_path }}
${{ steps.deb.outputs.deb_path }}