Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions .github/workflows/build-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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' }}
Expand All @@ -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: |
Expand Down Expand Up @@ -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
37 changes: 33 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1481,6 +1482,34 @@ placement and a version skew would change chroma per-OS.
- Show in Folder: `xdg-open <directory>`
- 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-<arch>.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.

Expand Down Expand Up @@ -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 |
Expand Down
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -68,11 +68,15 @@ VapourBox is signed with an Apple Developer ID and notarized, so it opens withou
<details>
<summary><b>Installing on Linux</b></summary>

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.

</details>
Expand Down
4 changes: 2 additions & 2 deletions Scripts/ci-build-and-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions Scripts/generate-app-icons.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
151 changes: 141 additions & 10 deletions Scripts/package-linux.sh
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -118,28 +132,145 @@ 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"

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"
Expand Down
Loading
Loading