diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 554da86e..74925cd7 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -35,6 +35,9 @@ jobs: sudo apt-get update sudo apt-get install -y clang cmake git ninja-build pkg-config \ libgtk-3-dev liblzma-dev libstdc++-12-dev + # zsyncmake writes the .zsync the AppImage's update information names; + # desktop-file-validate lets appimagetool check the desktop entry. + sudo apt-get install -y zsync desktop-file-utils - name: Setup Flutter uses: subosito/flutter-action@v2 @@ -113,7 +116,11 @@ jobs: uses: actions/upload-artifact@v5 with: name: VapourBox-${{ inputs.version }}-linux-x64 - path: dist/VapourBox-${{ inputs.version }}-linux-x64.tar.gz + if-no-files-found: error + path: | + dist/VapourBox-${{ inputs.version }}-linux-x64.AppImage + dist/VapourBox-${{ inputs.version }}-linux-x64.AppImage.zsync + dist/VapourBox-${{ inputs.version }}-linux-x64.tar.gz build-arm64: if: ${{ inputs.arch == 'both' || inputs.arch == 'arm64' }} @@ -128,6 +135,9 @@ jobs: sudo apt-get update sudo apt-get install -y clang cmake git ninja-build pkg-config \ libgtk-3-dev liblzma-dev libstdc++-12-dev + # zsyncmake writes the .zsync the AppImage's update information names; + # desktop-file-validate lets appimagetool check the desktop entry. + sudo apt-get install -y zsync desktop-file-utils - name: Setup Flutter run: | @@ -211,4 +221,8 @@ jobs: uses: actions/upload-artifact@v5 with: name: VapourBox-${{ inputs.version }}-linux-arm64 - path: dist/VapourBox-${{ inputs.version }}-linux-arm64.tar.gz + if-no-files-found: error + path: | + dist/VapourBox-${{ inputs.version }}-linux-arm64.AppImage + dist/VapourBox-${{ inputs.version }}-linux-arm64.AppImage.zsync + dist/VapourBox-${{ inputs.version }}-linux-arm64.tar.gz diff --git a/CLAUDE.md b/CLAUDE.md index aeb410bb..0deed686 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -266,8 +266,8 @@ dart run build_runner build # Windows .\Scripts\package-windows.ps1 -Version "X.Y.Z" [-SkipBuild] -# Linux -./Scripts/package-linux.sh --version X.Y.Z [--skip-build] +# Linux — writes an AppImage (+ its .zsync) and a tarball of the same tree +./Scripts/package-linux.sh --version X.Y.Z [--skip-build] [--skip-appimage] # Test packaged app from terminal to see errors: # macOS: @@ -397,7 +397,8 @@ Adding a filter touches many files. Missing any step causes silent failures (fil icon with CoreGraphics at whatever size you ask for, so no ImageMagick or librsvg is needed (and every size is drawn at its own scale rather than downsampled from one bitmap). `Scripts/generate-app-icons.sh` drives it at the -sizes each platform wants and packs the Windows `.ico` in pure Python: +sizes each platform wants, packs the Windows `.ico` in pure Python, and writes +the Linux AppImage icon (`packaging/linux/app.vapourbox.VapourBox.png`): ```bash ./Scripts/generate-app-icons.sh # macOS only; needs Swift from the Xcode CLT @@ -1481,6 +1482,34 @@ placement and a version skew would change chroma per-OS. - Show in Folder: `xdg-open ` - Debug builds: use `./Scripts/run-debug-linux.sh` - No code signing or quarantine removal needed +- **Ships as an AppImage, with a tarball of the same tree as the fallback** + (`package-linux.sh`; pieces in `packaging/linux/`). The rules that keep it + working: + - **`AppRun` exports nothing** — no `LD_LIBRARY_PATH`, no bundled GTK. Its + environment is inherited by the worker and by the ffmpeg/vspipe it spawns + from the downloaded deps bundle, so anything set there leaks into them. + - **The mount is read-only** (`/tmp/.mount_*`). Nothing may be written next + to the executable on Linux; deps and add-ons already live under + `$XDG_DATA_HOME`, which is also why deps stay a first-launch download + instead of being packed in (they are versioned separately and come in two + CPU tiers). + - **The `.AppImage.zsync` must be uploaded beside every AppImage.** The + embedded update information names it + (`gh-releases-zsync|…|latest|VapourBox-*-linux-.AppImage.zsync`), so + one missing is an updater that fails for every user. The script fails if + `zsyncmake` is absent rather than build without it, and + `packaging_test.dart` asserts the release upload picks both files up. + `latest` is GitHub's Latest release — one more reason a deps or whisper + release must never be marked Latest. + - The desktop file and icon are named for `APPLICATION_ID` + (`app.vapourbox.VapourBox`, `app/linux/CMakeLists.txt`), which is what + the window reports as its app id; rename one and the running window stops + matching its launcher. + - `appimagetool` is pinned by version **and** SHA-256 in the script. The + AppImage runtime it embeds is fetched by appimagetool itself at build time + and is not pinned. + - Not verifiable from macOS (appimagetool is a Linux binary): + `build-linux.yml` is the test. Plugin lists for all platforms: see `deps/` directories or download scripts. @@ -1583,7 +1612,7 @@ Notes: | `Scripts/check-deps-changed.sh` | Detect if deps changed since last release (exit 0=changed, 1=unchanged) | | `Scripts/package-macos.sh` | Package macOS app | | `Scripts/package-windows.ps1` | Package Windows app | -| `Scripts/package-linux.sh` | Package Linux app | +| `Scripts/package-linux.sh` | Package Linux app (AppImage + `.zsync` + tarball) | | `Scripts/package-deps-macos.sh` | Package macOS deps | | `Scripts/package-deps-windows.ps1` | Package Windows deps | | `Scripts/package-deps-linux.sh` | Package Linux deps | diff --git a/README.md b/README.md index 5de2655d..6f261828 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,8 @@ VapourBox runs [VapourSynth](https://www.vapoursynth.com/), QTGMC and FFmpeg — | macOS (Apple Silicon) | macOS 15 Sequoia | `VapourBox-x.x.x-macos-arm64.dmg` | | macOS (Intel) | macOS 12 Monterey | `VapourBox-x.x.x-macos-x64.dmg` | | Windows (x64) | Windows 10/11 | `VapourBox-x.x.x-windows-x64.zip` | -| Linux (x64) | glibc 2.39 (Ubuntu 24.04, Debian 13) | `VapourBox-x.x.x-linux-x64.tar.gz` | -| Linux (arm64) | glibc 2.39 (Ubuntu 24.04, Debian 13) | `VapourBox-x.x.x-linux-arm64.tar.gz` | +| Linux (x64) | glibc 2.39 (Ubuntu 24.04, Debian 13) | `VapourBox-x.x.x-linux-x64.AppImage` | +| Linux (arm64) | glibc 2.39 (Ubuntu 24.04, Debian 13) | `VapourBox-x.x.x-linux-arm64.AppImage` | All processing is local. There is no account, no telemetry, and nothing is uploaded. @@ -68,11 +68,15 @@ VapourBox is signed with an Apple Developer ID and notarized, so it opens withou
Installing on Linux -1. Download the `.tar.gz` for your architecture. -2. `tar -xzf VapourBox-x.x.x-linux-x64.tar.gz` -3. `cd VapourBox-x.x.x-linux-x64 && ./vapourbox` +1. Download the `.AppImage` for your architecture. +2. Make it executable: `chmod +x VapourBox-x.x.x-linux-x64.AppImage` (or tick *Allow executing file as program* in the file manager's Properties). +3. Run it: `./VapourBox-x.x.x-linux-x64.AppImage` 4. First launch downloads its processing dependencies (~185 MB). +The AppImage is a single file that needs no installation and can live anywhere. To get a menu entry and icon, open it with an AppImage manager such as [Gear Lever](https://github.com/mijorus/gearlever) or [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher); those, and [AppImageUpdate](https://github.com/AppImageCommunity/AppImageUpdate), can also update it in place, downloading only what changed. + +Each release also carries a plain `.tar.gz` of the same build, for systems where an AppImage can't be mounted: extract it and run `./vapourbox`. + GPU-accelerated deinterlacing (NNEDI3CL) needs your GPU's OpenCL driver installed. Without it, VapourBox falls back to the CPU automatically.
diff --git a/Scripts/ci-build-and-release.sh b/Scripts/ci-build-and-release.sh index 8da4768c..f92ccf82 100755 --- a/Scripts/ci-build-and-release.sh +++ b/Scripts/ci-build-and-release.sh @@ -185,7 +185,7 @@ echo " Downloading Linux artifact..." gh run download "$LINUX_RUN_ID" --dir "$DOWNLOAD_DIR" echo " Downloaded artifacts:" -find "$DOWNLOAD_DIR" -type f \( -name "*.zip" -o -name "*.dmg" -o -name "*.tar.gz" -o -name "*.sha256.json" \) | while read -r f; do +find "$DOWNLOAD_DIR" -type f \( -name "*.zip" -o -name "*.dmg" -o -name "*.tar.gz" -o -name "*.AppImage" -o -name "*.AppImage.zsync" -o -name "*.sha256.json" \) | while read -r f; do SIZE=$(du -sh "$f" | cut -f1) echo " $(basename "$f") ($SIZE)" done @@ -195,7 +195,7 @@ echo "" echo -e "${BLUE}[4/4] Uploading to release ${RELEASE_TAG}...${NC}" UPLOAD_COUNT=0 -find "$DOWNLOAD_DIR" -type f \( -name "*.zip" -o -name "*.dmg" -o -name "*.tar.gz" -o -name "*.sha256.json" \) | while read -r f; do +find "$DOWNLOAD_DIR" -type f \( -name "*.zip" -o -name "*.dmg" -o -name "*.tar.gz" -o -name "*.AppImage" -o -name "*.AppImage.zsync" -o -name "*.sha256.json" \) | while read -r f; do BASENAME=$(basename "$f") echo " Uploading $BASENAME..." gh release upload "$RELEASE_TAG" "$f" --clobber diff --git a/Scripts/generate-app-icons.sh b/Scripts/generate-app-icons.sh index 80607315..2b2678d8 100755 --- a/Scripts/generate-app-icons.sh +++ b/Scripts/generate-app-icons.sh @@ -20,6 +20,7 @@ PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" RENDER="$PROJECT_ROOT/Scripts/generate-app-icon.swift" ICONSET="$PROJECT_ROOT/app/macos/Runner/Assets.xcassets/AppIcon.appiconset" WIN_RES="$PROJECT_ROOT/app/windows/runner/resources" +LINUX_ICON="$PROJECT_ROOT/packaging/linux/app.vapourbox.VapourBox.png" if ! command -v swift >/dev/null 2>&1; then echo "ERROR: swift not found — install the Xcode command line tools." >&2 @@ -72,6 +73,11 @@ pathlib.Path(out).write_bytes(header + directory + b''.join(blobs)) print(f' {out} ({len(sizes)} sizes: {", ".join(map(str, sizes))})') PY +echo "==> Linux AppImage icon" +# One 256px PNG: the size the AppImage format asks for at the AppDir root, and +# what package-linux.sh installs under hicolor/256x256. +swift "$RENDER" --size 256 --style square --out "$LINUX_ICON" + echo "" echo "Done. Rebuild the app to see them:" echo " ./Scripts/run-debug-macos.sh --skip-worker" diff --git a/Scripts/package-linux.sh b/Scripts/package-linux.sh index 4ff7fb0c..215a3f75 100755 --- a/Scripts/package-linux.sh +++ b/Scripts/package-linux.sh @@ -1,27 +1,38 @@ #!/bin/bash # Package VapourBox for Linux -# Creates a distributable tarball with Flutter app + Rust worker + templates +# Creates an AppImage (the primary download) and a tarball (the fallback) from +# the same tree: Flutter app + Rust worker + templates. # # Prerequisites: # - Flutter SDK, Rust toolchain # - Worker and app already built (or use without --skip-build) +# - For the AppImage: zsyncmake (apt install zsync), and a host of the target +# architecture — appimagetool is fetched automatically, or set $APPIMAGETOOL # # Usage: ./Scripts/package-linux.sh --version X.Y.Z [--skip-build] [--arch x64|arm64] +# [--skip-appimage] set -e VERSION="1.0.0" SKIP_BUILD=false +SKIP_APPIMAGE=false ARCH="" +# Pinned, and checksummed below: this binary assembles what users run. +APPIMAGETOOL_VERSION="1.9.1" +GITHUB_REPO="StuartCameronCode/VapourBox" +APP_ID="app.vapourbox.VapourBox" + while [[ $# -gt 0 ]]; do case $1 in --version) VERSION="$2"; shift 2 ;; --skip-build) SKIP_BUILD=true; shift ;; --arch) ARCH="$2"; shift 2 ;; + --skip-appimage) SKIP_APPIMAGE=true; shift ;; *) echo "Unknown option: $1" - echo "Usage: $0 --version X.Y.Z [--skip-build] [--arch x64|arm64]" + echo "Usage: $0 --version X.Y.Z [--skip-build] [--arch x64|arm64] [--skip-appimage]" exit 1 ;; esac @@ -51,6 +62,9 @@ TOTAL_STEPS=4 if ! $SKIP_BUILD; then TOTAL_STEPS=5 fi +if ! $SKIP_APPIMAGE; then + TOTAL_STEPS=$((TOTAL_STEPS + 1)) +fi # Build if needed if ! $SKIP_BUILD; then @@ -118,16 +132,122 @@ cp -r "$PROJECT_ROOT/licenses/"* "$PACKAGE_DIR/licenses/" 2>/dev/null || true STEP=$((STEP + 1)) +# Create AppImage +# +# The AppImage holds exactly the tree the tarball does, under usr/lib/vapourbox, +# so the two cannot drift. Nothing inside it is written to at runtime: deps and +# add-ons install under $XDG_DATA_HOME, which is also why the processing +# dependencies stay a first-launch download rather than being packed in here. +APPIMAGE_FILE="" +if ! $SKIP_APPIMAGE; then + echo "[$STEP/$TOTAL_STEPS] Creating AppImage..." + + if [ "$(uname -s)" != "Linux" ]; then + echo "ERROR: an AppImage can only be built on Linux (or pass --skip-appimage)." + exit 1 + fi + + case "$ARCH" in + x64) + AI_ARCH="x86_64" + AI_SHA256="ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0" + ;; + arm64) + AI_ARCH="aarch64" + AI_SHA256="f0837e7448a0c1e4e650a93bb3e85802546e60654ef287576f46c71c126a9158" + ;; + *) + echo "ERROR: no AppImage architecture for --arch $ARCH" + exit 1 + ;; + esac + + if [ "$(uname -m)" != "$AI_ARCH" ]; then + echo "ERROR: building a $AI_ARCH AppImage needs a $AI_ARCH host (this is $(uname -m))." + echo " appimagetool is a native binary. Pass --skip-appimage for the tarball alone." + exit 1 + fi + + # Without zsyncmake appimagetool still succeeds, and still embeds update + # information — pointing at a .zsync that was never made. That is an + # updater that fails for every user, so it is an error here, not a warning. + if ! command -v zsyncmake >/dev/null 2>&1; then + echo "ERROR: zsyncmake not found (apt install zsync)." + echo " It generates the .zsync file the AppImage's update information points at." + exit 1 + fi + + if [ -z "$APPIMAGETOOL" ]; then + TOOLS_DIR="$DIST_DIR/.tools" + APPIMAGETOOL="$TOOLS_DIR/appimagetool-$APPIMAGETOOL_VERSION-$AI_ARCH.AppImage" + if [ ! -f "$APPIMAGETOOL" ]; then + mkdir -p "$TOOLS_DIR" + curl -fsSL -o "$APPIMAGETOOL.part" \ + "https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-$AI_ARCH.AppImage" + if ! echo "$AI_SHA256 $APPIMAGETOOL.part" | sha256sum -c - >/dev/null; then + echo "ERROR: appimagetool $APPIMAGETOOL_VERSION ($AI_ARCH) failed its checksum." + rm -f "$APPIMAGETOOL.part" + exit 1 + fi + mv "$APPIMAGETOOL.part" "$APPIMAGETOOL" + fi + chmod +x "$APPIMAGETOOL" + fi + + LINUX_PACKAGING="$PROJECT_ROOT/packaging/linux" + APPDIR="$DIST_DIR/$PACKAGE_NAME.AppDir" + rm -rf "$APPDIR" + mkdir -p "$APPDIR/usr/lib" \ + "$APPDIR/usr/share/applications" \ + "$APPDIR/usr/share/icons/hicolor/256x256/apps" + cp -r "$PACKAGE_DIR" "$APPDIR/usr/lib/vapourbox" + + install -m 755 "$LINUX_PACKAGING/AppRun" "$APPDIR/AppRun" + # appimagetool wants the desktop file and its icon at the root; the copies + # under usr/share are what desktop-integration tools install from. + cp "$LINUX_PACKAGING/$APP_ID.desktop" "$APPDIR/" + cp "$LINUX_PACKAGING/$APP_ID.desktop" "$APPDIR/usr/share/applications/" + cp "$LINUX_PACKAGING/$APP_ID.png" "$APPDIR/" + cp "$LINUX_PACKAGING/$APP_ID.png" "$APPDIR/usr/share/icons/hicolor/256x256/apps/" + ln -s "$APP_ID.png" "$APPDIR/.DirIcon" + + # "latest" follows GitHub's Latest release, which is why a deps or whisper + # release must never be marked Latest (see CLAUDE.md). The wildcard stands + # in for the version; the arch stays literal so x64 never updates to arm64. + APPIMAGE_FILE="$PACKAGE_NAME.AppImage" + UPDATE_INFO="gh-releases-zsync|${GITHUB_REPO%%/*}|${GITHUB_REPO##*/}|latest|VapourBox-*-linux-$ARCH.AppImage.zsync" + + # appimagetool writes the .zsync into the working directory. It is itself + # an AppImage; extract-and-run spares the build host from needing FUSE. + cd "$DIST_DIR" + rm -f "$APPIMAGE_FILE" "$APPIMAGE_FILE.zsync" + ARCH="$AI_ARCH" APPIMAGE_EXTRACT_AND_RUN=1 VERSION="$VERSION" \ + "$APPIMAGETOOL" --updateinformation "$UPDATE_INFO" "$APPDIR" "$APPIMAGE_FILE" + + if [ ! -s "$APPIMAGE_FILE" ] || [ ! -s "$APPIMAGE_FILE.zsync" ]; then + echo "ERROR: appimagetool did not produce both $APPIMAGE_FILE and $APPIMAGE_FILE.zsync" + exit 1 + fi + chmod +x "$APPIMAGE_FILE" + rm -rf "$APPDIR" + + STEP=$((STEP + 1)) +fi + # Create tarball echo "[$STEP/$TOTAL_STEPS] Creating tarball..." cd "$DIST_DIR" TAR_FILE="$PACKAGE_NAME.tar.gz" tar -czf "$TAR_FILE" "$PACKAGE_NAME" -# Calculate size and hash -TAR_SIZE=$(stat -c%s "$TAR_FILE" 2>/dev/null || stat -f%z "$TAR_FILE" 2>/dev/null) -TAR_SIZE_MB=$(echo "scale=1; $TAR_SIZE / 1048576" | bc) -SHA256=$(sha256sum "$TAR_FILE" 2>/dev/null | cut -d' ' -f1 || shasum -a 256 "$TAR_FILE" | cut -d' ' -f1) +file_size_mb() { + local bytes + bytes=$(stat -c%s "$1" 2>/dev/null || stat -f%z "$1" 2>/dev/null) + echo "scale=1; $bytes / 1048576" | bc +} +file_sha256() { + sha256sum "$1" 2>/dev/null | cut -d' ' -f1 || shasum -a 256 "$1" | cut -d' ' -f1 +} # Cleanup unpacked directory rm -rf "$PACKAGE_DIR" @@ -135,11 +255,22 @@ rm -rf "$PACKAGE_DIR" echo "" echo "=== Packaging Complete ===" echo "" -echo " Tarball: $DIST_DIR/$TAR_FILE" -echo " Size: ${TAR_SIZE_MB} MB" -echo " SHA256: $SHA256" +if [ -n "$APPIMAGE_FILE" ]; then + echo " AppImage: $DIST_DIR/$APPIMAGE_FILE" + echo " Size: $(file_size_mb "$APPIMAGE_FILE") MB" + echo " SHA256: $(file_sha256 "$APPIMAGE_FILE")" + echo " Update: $DIST_DIR/$APPIMAGE_FILE.zsync (upload it beside the AppImage)" + echo "" +fi +echo " Tarball: $DIST_DIR/$TAR_FILE" +echo " Size: $(file_size_mb "$TAR_FILE") MB" +echo " SHA256: $(file_sha256 "$TAR_FILE")" echo "" -echo "To install:" +echo "To run:" +if [ -n "$APPIMAGE_FILE" ]; then + echo " chmod +x $APPIMAGE_FILE && ./$APPIMAGE_FILE" + echo "or:" +fi echo " tar -xzf $TAR_FILE" echo " cd $PACKAGE_NAME" echo " ./vapourbox" diff --git a/Scripts/release.sh b/Scripts/release.sh index 12971adb..9ab32ff9 100755 --- a/Scripts/release.sh +++ b/Scripts/release.sh @@ -306,13 +306,13 @@ RELEASE_NOTES="## VapourBox $APP_VERSION - **Windows**: \`VapourBox-$APP_VERSION-windows-x64.zip\` - **macOS (Apple Silicon)**: \`VapourBox-$APP_VERSION-macos-arm64.zip\` (contains signed & notarized DMG) - **macOS (Intel)**: \`VapourBox-$APP_VERSION-macos-x64.zip\` (contains signed & notarized DMG) -- **Linux (x64)**: \`VapourBox-$APP_VERSION-linux-x64.tar.gz\` -- **Linux (arm64)**: \`VapourBox-$APP_VERSION-linux-arm64.tar.gz\` +- **Linux (x64)**: \`VapourBox-$APP_VERSION-linux-x64.AppImage\` (or the \`.tar.gz\`) +- **Linux (arm64)**: \`VapourBox-$APP_VERSION-linux-arm64.AppImage\` (or the \`.tar.gz\`) ### Installation - **Windows**: Extract zip and run \`vapourbox.exe\` - **macOS**: Open the DMG and drag VapourBox to Applications -- **Linux**: Extract tarball and run \`./vapourbox\` +- **Linux**: \`chmod +x\` the AppImage and run it — or extract the tarball and run \`./vapourbox\` ### Dependencies Dependencies are automatically downloaded on first launch (~185 MB). @@ -345,7 +345,7 @@ echo -e "${GREEN}╚════════════════════ echo "" echo "Next steps:" echo " 1. Wait for Windows and Linux builds to complete (if using GitHub Actions)" -echo " 2. Upload Windows zip and Linux tarballs to the draft release" +echo " 2. Upload the Windows zip and the Linux AppImages (each with its .zsync) and tarballs to the draft release" echo " 3. Test the release builds" echo " 4. Edit release notes" echo " 5. Publish the release" diff --git a/app/test/packaging_test.dart b/app/test/packaging_test.dart index 7cc1d614..3fcf067a 100644 --- a/app/test/packaging_test.dart +++ b/app/test/packaging_test.dart @@ -29,6 +29,13 @@ String _repoRoot() { } } +/// Reads a repo file with LF endings. A Windows checkout gives these files +/// CRLF, so an assertion on a whole line (`'Icon=$id\n'`) fails there only. +String _read(String root, List parts) => + File(p.joinAll([root, ...parts])) + .readAsStringSync() + .replaceAll('\r\n', '\n'); + void main() { final root = _repoRoot(); @@ -72,4 +79,56 @@ void main() { ); }); } + + // The Linux AppImage embeds update information naming a `.zsync` on the + // latest GitHub release. Three files have to agree on that name, and nothing + // fails at build time if they don't — the updater just finds nothing. + group('Linux AppImage update information', () { + final package = _read(root, ['Scripts', 'package-linux.sh']); + + test('names the .zsync the script itself produces', () { + expect(package, contains(r'APPIMAGE_FILE="$PACKAGE_NAME.AppImage"')); + expect( + package, contains(r'PACKAGE_NAME="VapourBox-$VERSION-linux-$ARCH"')); + expect( + package, + contains(r'|latest|VapourBox-*-linux-$ARCH.AppImage.zsync"'), + reason: 'the update pattern must be the output filename with the ' + 'version wildcarded, plus .zsync', + ); + }); + + test('the release upload carries the AppImage and its .zsync', () { + final upload = _read(root, ['Scripts', 'ci-build-and-release.sh']); + final finds = upload + .split('\n') + .where((l) => l.startsWith('find ') && l.contains('*.tar.gz')) + .toList(); + expect(finds, isNotEmpty); + for (final line in finds) { + expect(line, contains('"*.AppImage"')); + expect(line, contains('"*.AppImage.zsync"')); + } + + final workflow = _read(root, ['.github', 'workflows', 'build-linux.yml']); + for (final arch in ['x64', 'arm64']) { + expect(workflow, contains('-linux-$arch.AppImage\n')); + expect(workflow, contains('-linux-$arch.AppImage.zsync\n')); + } + }); + + test('the desktop entry and icon are named for the application id', () { + final cmake = _read(root, ['app', 'linux', 'CMakeLists.txt']); + final id = RegExp(r'set\(APPLICATION_ID "([^"]+)"\)') + .firstMatch(cmake)! + .group(1)!; + final dir = p.join(root, 'packaging', 'linux'); + expect(File(p.join(dir, '$id.desktop')).existsSync(), isTrue); + expect(File(p.join(dir, '$id.png')).existsSync(), isTrue); + final body = _read(root, ['packaging', 'linux', '$id.desktop']); + expect(body, contains('Icon=$id\n')); + expect(body, contains('StartupWMClass=$id\n')); + expect(package, contains('APP_ID="$id"')); + }); + }); } diff --git a/docs/BUILDING.md b/docs/BUILDING.md index cce5a0f6..0b54570b 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -246,7 +246,8 @@ dart run build_runner build --delete-conflicting-outputs `Scripts/generate-app-icon.swift` is the vector source of truth (CoreGraphics, no ImageMagick needed); `Scripts/generate-app-icons.sh` drives it at every size each -platform wants and packs the Windows `.ico`. macOS only, needs Swift from the CLT: +platform wants, packs the Windows `.ico` and writes the Linux AppImage icon +(`packaging/linux/app.vapourbox.VapourBox.png`). macOS only, needs Swift from the CLT: ```bash ./Scripts/generate-app-icons.sh @@ -269,7 +270,15 @@ Output: `dist/VapourBox-1.0.0-windows-x64.zip` ```bash ./Scripts/package-linux.sh --version 1.0.0 [--skip-build] [--arch x64|arm64] ``` -`--arch` defaults to the host. Output: `dist/VapourBox-1.0.0-linux-.tar.gz` +`--arch` defaults to the host. Output, all from the same tree: + +- `dist/VapourBox-1.0.0-linux-.AppImage` — the primary download +- `dist/VapourBox-1.0.0-linux-.AppImage.zsync` — its delta-update index; must be uploaded beside it +- `dist/VapourBox-1.0.0-linux-.tar.gz` — the fallback + +The AppImage step needs `zsyncmake` (`sudo apt install zsync`) and a host of the +target architecture; it downloads a pinned, checksummed `appimagetool` into +`dist/.tools/` (or uses `$APPIMAGETOOL`). `--skip-appimage` builds the tarball alone. ### macOS ```bash diff --git a/packaging/linux/AppRun b/packaging/linux/AppRun new file mode 100755 index 00000000..a4bda984 --- /dev/null +++ b/packaging/linux/AppRun @@ -0,0 +1,10 @@ +#!/bin/sh +# AppImage entry point. +# +# Deliberately sets no LD_LIBRARY_PATH (or anything else): the Flutter runner +# finds its own lib/ through an $ORIGIN rpath, and whatever this exports is +# inherited by the worker and by the ffmpeg/vspipe it spawns from the +# downloaded deps bundle, which must see only the environment the worker gives +# them. +HERE="$(dirname "$(readlink -f "$0")")" +exec "$HERE/usr/lib/vapourbox/vapourbox" "$@" diff --git a/packaging/linux/app.vapourbox.VapourBox.desktop b/packaging/linux/app.vapourbox.VapourBox.desktop new file mode 100644 index 00000000..b9c7bd16 --- /dev/null +++ b/packaging/linux/app.vapourbox.VapourBox.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +Type=Application +Name=VapourBox +GenericName=Video Processor +Comment=Deinterlace, restore and convert video with VapourSynth and QTGMC +Exec=vapourbox +Icon=app.vapourbox.VapourBox +Terminal=false +Categories=AudioVideo;Video; +Keywords=deinterlace;QTGMC;VapourSynth;VHS;DV;DVD;convert; +StartupNotify=true +StartupWMClass=app.vapourbox.VapourBox diff --git a/packaging/linux/app.vapourbox.VapourBox.png b/packaging/linux/app.vapourbox.VapourBox.png new file mode 100644 index 00000000..36882d44 Binary files /dev/null and b/packaging/linux/app.vapourbox.VapourBox.png differ