Build Linux (tar.gz + AppImage) #23
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }} |