diff --git a/.github/workflows/tauri-build.yml b/.github/workflows/tauri-build.yml index 9de2f3de2..9df65cd75 100644 --- a/.github/workflows/tauri-build.yml +++ b/.github/workflows/tauri-build.yml @@ -741,6 +741,91 @@ jobs: retention-days: 1 include-hidden-files: true + linux-wry: + name: Build Linux x86_64 (wry) + needs: setup-release + runs-on: ubuntu-22.04 + timeout-minutes: 90 + permissions: + contents: write + id-token: write + attestations: write + artifact-metadata: write + env: + ARCH: x86_64 + TAG: ${{ needs.setup-release.outputs.tag }} + VERSION: ${{ needs.setup-release.outputs.version }} + VITE_APP_VERSION: ${{ needs.setup-release.outputs.version }} + SABLE_BUILD_FLAVOR: ${{ needs.setup-release.outputs.nightly == 'true' && 'dev' || 'stable' }} + VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }} + VITE_SENTRY_ENVIRONMENT: ${{ needs.setup-release.outputs.nightly == 'true' && 'preview' || 'production' }} + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} + IS_NIGHTLY: ${{ needs.setup-release.outputs.nightly }} + MISE_ENV: tauri + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.setup-release.outputs.ref }} + persist-credentials: false + + - name: Setup app + uses: ./.github/actions/setup + with: + tauri: 'true' + + - name: Cache Rust build + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + workspaces: src-tauri + key: wry-x86_64 + + - name: Stamp release version into tauri.conf.json + shell: bash + env: + UPDATER_ENDPOINT: ${{ needs.setup-release.outputs.nightly == 'true' && format('https://github.com/{0}/releases/download/nightly/latest.json', github.repository) || '' }} + run: node .github/scripts/set-tauri-version.mjs "$VERSION" "$UPDATER_ENDPOINT" + + - name: Build and package Linux bundles + shell: bash + run: | + pnpm tauri:wry build --no-bundle + ! strings src-tauri/target/release/sable | grep -F 'http://localhost:8080' + cp src-tauri/target/release/sable src-tauri/target/release/sable-updater + pnpm tauri:wry build --no-bundle --no-updater + + DISPLAY_NAME=Sable + [ "$IS_NIGHTLY" = "true" ] && DISPLAY_NAME="Sable Nightly" + mise run webkit:package "$VERSION" \ + src-tauri/target/release/sable "$DISPLAY_NAME" \ + src-tauri/target/release/sable-updater + + test -f src-tauri/target/release/webkit-pkg/stage/runtime/webkit/WEBKITGTK-LICENSE.txt + + - name: Sign the AppImage for the updater + if: ${{ env.TAURI_SIGNING_PRIVATE_KEY != '' }} + shell: bash + run: | + for f in src-tauri/target/release/bundle/appimage/Sable-*-linux-${ARCH}.AppImage; do + [ -e "$f" ] || continue + pnpm tauri signer sign "$f" + done + + - name: Upload wry bundles as workflow artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: sable-linux-wry-${{ env.ARCH }} + path: | + src-tauri/target/release/bundle/deb/Sable-*.deb + src-tauri/target/release/bundle/rpm/Sable-*.rpm + src-tauri/target/release/bundle/appimage/Sable-*.AppImage + if-no-files-found: error + retention-days: 7 + include-hidden-files: true + distribute-nightly-aur: name: Publish nightly to AUR needs: [setup-release, linux] diff --git a/nfpm.webkit.yaml b/nfpm.webkit.yaml new file mode 100644 index 000000000..a24580752 --- /dev/null +++ b/nfpm.webkit.yaml @@ -0,0 +1,91 @@ +# yaml-language-server: $schema=https://nfpm.goreleaser.com/schema.json +# wry flavour. The embedded WebKit runtime carries its own private libraries +# (ICU, libsoup3, GStreamer, bwrap), so the only dependencies left are the ones +# the app and the engine must share a single instance of. + +name: sable +arch: ${PKG_ARCH} +platform: linux +version: ${PKG_VERSION} +release: ${PKG_RELEASE} +version_schema: semver +section: net +maintainer: SableClient +description: Sable, a Matrix client +homepage: https://sable.moe + +contents: + - src: ${PKGROOT}/opt/sable/ + dst: /opt/sable + type: tree + expand: true + - src: ${PKGROOT}/usr/ + dst: /usr + type: tree + expand: true + - src: src-tauri/packaging/deb/apparmor/sable + dst: /etc/apparmor.d/sable + type: config + packager: deb + +overrides: + deb: + scripts: + postinstall: src-tauri/packaging/deb/postinst + preremove: src-tauri/packaging/deb/prerm + depends: + - apparmor + - libgtk-3-0 + - libglib2.0-0 + - libcairo2 + - libpango-1.0-0 + - libgdk-pixbuf-2.0-0 + - libatk1.0-0 + - libharfbuzz0b + - libfreetype6 + - libfontconfig1 + - libx11-6 + - libgl1 + - libegl1 + - libxkbcommon0 + - libxtst6 + - libxss1 + - libatomic1 + - libharfbuzz-icu0 + - libnghttp2-14 + - librsvg2-2 + - libpsl5 + - libtasn1-6 + - libwayland-client0 + - libwayland-egl1 + - libxcb1 + - xdg-utils + - libayatana-appindicator3-1 + rpm: + depends: + - gtk3 + - glib2 + - cairo + - pango + - gdk-pixbuf2 + - atk + - harfbuzz + - freetype + - fontconfig + - libX11 + - mesa-libGL + - mesa-libEGL + - libxkbcommon + - libXtst + - libXScrnSaver + - libatomic + - harfbuzz-icu + - libnghttp2 + - librsvg2 + - libpsl + - libtasn1 + - libwayland-client + - libwayland-egl + - libxcb + - xdg-utils + - libayatana-appindicator3.so.1()(64bit) diff --git a/scripts/webkit/copy-libs.sh b/scripts/webkit/copy-libs.sh new file mode 100755 index 000000000..3c032b840 --- /dev/null +++ b/scripts/webkit/copy-libs.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +#MISE description="Copy the WebRTC WebKitGTK runtime next to the built binary" +# Copy the portable WebKitGTK runtime next to a built binary so wry builds get +# WebRTC regardless of the host's webkit2gtk. The bundle carries its own rpaths +# and private dependencies. +# +# Usage: scripts/webkit/copy-libs.sh [debug|release] [dest-dir] +# dest-dir defaults to src-tauri/target/ (beside the built binary). +set -euo pipefail +PROFILE="${1:-debug}" +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +DEST="${2:-$ROOT/src-tauri/target/$PROFILE}" + +if [ "$(uname -m)" != x86_64 ]; then + echo "❌ the WebKit runtime is only built for x86_64 (got $(uname -m))." >&2 + exit 1 +fi + +SRC="${SABLE_WEBKIT_RUNTIME_DIR:-}" +if [ -z "$SRC" ]; then + CACHE="$ROOT/src-tauri/target/webkit-runtime" + bash "$ROOT/scripts/webkit/fetch.sh" "$CACHE" + SRC="$CACHE/runtime" +fi + +WEBKIT_LIB="$SRC/libwebkit2gtk-4.1.so.0" +[ -f "$WEBKIT_LIB" ] || { + echo "❌ libwebkit2gtk-4.1.so.0 not found in $SRC." >&2 + exit 1 +} + +# Without the patch the library silently forks the host's helpers instead. +# Process substitution, not a pipe: grep -q exits early and SIGPIPEs strings, +# which trips pipefail. +if ! grep -qx WEBKIT_EXEC_PATH < <(strings -a "$WEBKIT_LIB"); then + echo "❌ $WEBKIT_LIB was built without relocatable-exec-path.patch." >&2 + echo " Re-pin scripts/webkit/runtime.json at a patched build." >&2 + exit 1 +fi + +NEED="$(node -e 'process.stdout.write(require(process.argv[1]).glibc_floor)' \ + "$SRC/runtime.json" 2>/dev/null || echo 0)" +HAVE="$(ldd --version | sed -n '1s/.*[^0-9]\([0-9]\+\.[0-9]\+\)$/\1/p')" +if [ "$(printf '%s\n%s\n' "$NEED" "$HAVE" | sort -V | tail -1)" != "$HAVE" ]; then + echo "❌ runtime needs glibc $NEED, this host has $HAVE." >&2 + exit 1 +fi + +rm -rf "$DEST/webkit" +mkdir -p "$DEST" +cp -a "$SRC" "$DEST/webkit" + +echo "✅ WebKit runtime staged in $DEST/webkit (glibc floor $NEED)" diff --git a/scripts/webkit/fetch.sh b/scripts/webkit/fetch.sh new file mode 100755 index 000000000..af9a52411 --- /dev/null +++ b/scripts/webkit/fetch.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +#MISE description="Download the WebRTC-enabled WebKitGTK runtime" +# Downloads the pinned webkitgtk-webrtc portable artifact into a cache dir. +# +# Usage: scripts/webkit/fetch.sh [cache-dir] +# cache-dir defaults to src-tauri/target/webkit-runtime. +# +# Override the source with SABLE_WEBKIT_ARTIFACT_URL, or point +# SABLE_WEBKIT_RUNTIME_DIR at an already-unpacked tree to skip this entirely. +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +CACHE="${1:-$ROOT/src-tauri/target/webkit-runtime}" +MANIFEST="$ROOT/scripts/webkit/runtime.json" + +read -r REPO RUN ARTIFACT VERSION < <( + node -e ' + const m = require(process.argv[1]); + process.stdout.write([m.repository, m.run, m.artifact, m.version].join(" ") + "\n"); + ' "$MANIFEST" +) + +URL="${SABLE_WEBKIT_ARTIFACT_URL:-$REPO/actions/runs/$RUN/artifacts/$ARTIFACT}" +STAMP="$CACHE/.stamp" +WANT="$URL $VERSION" + +if [ -f "$STAMP" ] && [ "$(cat "$STAMP")" = "$WANT" ]; then + echo "→ WebKit runtime already cached in $CACHE" + exit 0 +fi + +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +echo "→ downloading $URL" +curl -fL --retry 3 --progress-bar -o "$WORK/artifact.zip" "$URL" +unzip -q -j "$WORK/artifact.zip" -d "$WORK" + +TARBALL="$(find "$WORK" -maxdepth 1 -name '*-portable-*.tar.zst' -print -quit)" +[ -n "$TARBALL" ] || { + echo "❌ no portable runtime tarball in the artifact." >&2 + exit 1 +} + +rm -rf "$CACHE" +mkdir -p "$CACHE" +tar --zstd -xf "$TARBALL" -C "$CACHE" + +echo "$WANT" > "$STAMP" +echo "✅ WebKit runtime $VERSION unpacked into $CACHE" diff --git a/scripts/webkit/package.sh b/scripts/webkit/package.sh new file mode 100755 index 000000000..dfffd1679 --- /dev/null +++ b/scripts/webkit/package.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +#MISE description="Package the wry build with the embedded WebKit runtime" +#MISE tools={nfpm="2.47.0", "github:AppImage/appimagetool" = {version = "1.9.1", matching = ".AppImage"}} +# Usage: scripts/webkit/package.sh [version] [package-binary-path] [display-name] [appimage-binary-path] +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT" + +VERSION="${1:-$(grep -m1 '"version":' src-tauri/tauri.conf.json | sed 's/.*: *"\(.*\)".*/\1/')}" +: "${VERSION:?version not found in src-tauri/tauri.conf.json}" +DEB_VERSION="$VERSION" +RPM_VERSION="$VERSION" +RPM_ITERATION=1 +if [[ "$VERSION" == *-* ]]; then + BASE_VERSION="${VERSION%%-*}" + PRERELEASE="${VERSION#*-}" + DEB_VERSION="${BASE_VERSION}~${PRERELEASE}" + RPM_VERSION="$BASE_VERSION" + RPM_ITERATION="0.${PRERELEASE}" +fi + +# The WebKit runtime is x86_64 only. +case "$(uname -m)" in + x86_64) export ARCH=x86_64; NFPM_ARCH=amd64 ;; + *) echo "unsupported architecture: $(uname -m)" >&2; exit 1 ;; +esac + +STAGE="$ROOT/src-tauri/target/release" +OUT="$STAGE/bundle" +WORK="$STAGE/webkit-pkg" + +BIN_PATH="${2:-}" +DISPLAY_NAME="${3:-}" +APPIMAGE_BIN_PATH="${4:-}" +if [ -z "$BIN_PATH" ]; then + for candidate in "$STAGE/Sable Nightly" "$STAGE/Sable" "$STAGE/sable" \ + "$ROOT/src-tauri/target/$ARCH-unknown-linux-gnu/release/sable"; do + [ -x "$candidate" ] || continue + BIN_PATH="$candidate" + break + done +fi +[ -n "$BIN_PATH" ] && [ -x "$BIN_PATH" ] || { + echo "no wry binary found; build it first (pnpm tauri:wry build --no-bundle)" >&2 + exit 1 +} +[ -n "$APPIMAGE_BIN_PATH" ] || APPIMAGE_BIN_PATH="$BIN_PATH" +[ -x "$APPIMAGE_BIN_PATH" ] || { + echo "AppImage binary is not executable: $APPIMAGE_BIN_PATH" >&2 + exit 1 +} +if [ -z "$DISPLAY_NAME" ]; then + case "$(basename "$BIN_PATH")" in + "Sable Nightly") DISPLAY_NAME="Sable Nightly" ;; + *) DISPLAY_NAME="Sable" ;; + esac +fi + +APPIMAGETOOL_CMD="" +if command -v appimagetool.AppImage >/dev/null 2>&1; then + APPIMAGETOOL_CMD="appimagetool.AppImage" +elif command -v appimagetool >/dev/null 2>&1; then + APPIMAGETOOL_CMD="appimagetool" +else + echo "appimagetool not found" >&2 + exit 1 +fi + +rm -rf "$WORK" +mkdir -p "$OUT/deb" "$OUT/rpm" "$OUT/appimage" + +bash scripts/webkit/stage.sh "$WORK/stage" "$DISPLAY_NAME" + +APPDIR="$WORK/Sable.AppDir" +mkdir -p "$APPDIR/usr/bin" +cp -a "$WORK/stage/runtime/." "$APPDIR/usr/bin/" +cp -f "$APPIMAGE_BIN_PATH" "$APPDIR/usr/bin/sable" +chmod 755 "$APPDIR/usr/bin/sable" + +# Bundle libayatana-appindicator3 and its closure for the tray, as CEF does. +stage_appindicator() { + local dest="$1" main dep + main="$(ldconfig -p 2>/dev/null | awk '$1=="libayatana-appindicator3.so.1"{v=$NF} END{print v}')" + [ -n "$main" ] || main="$(find /usr/lib /usr/lib64 /lib -name libayatana-appindicator3.so.1 2>/dev/null | sort | tail -n1)" + if [ -z "$main" ] || [ ! -e "$main" ]; then + echo "warning: libayatana-appindicator3.so.1 not found; tray disabled in the AppImage" >&2 + return 0 + fi + { + echo "$main" + ldd "$main" 2>/dev/null | awk '/=>/ {print $3}' | grep -iE 'ayatana|dbusmenu|indicator|ido' || true + } | sort -u | while read -r dep; do + if [ -e "$dep" ]; then + cp -Lf "$dep" "$dest/$(basename "$dep")" + fi + done +} +stage_appindicator "$APPDIR/usr/bin" + +if command -v nfpm >/dev/null 2>&1; then + PKGROOT="$WORK/pkgroot" + mkdir -p "$PKGROOT/opt/sable" "$PKGROOT/usr/bin" "$PKGROOT/usr/share/applications" + cp -a "$WORK/stage/runtime/." "$PKGROOT/opt/sable/" + cp -f "$BIN_PATH" "$PKGROOT/opt/sable/sable" + chmod 755 "$PKGROOT/opt/sable/sable" + cat > "$PKGROOT/usr/bin/sable" <<'EOF' +#!/bin/sh +exec /opt/sable/sable "$@" +EOF + chmod 755 "$PKGROOT/usr/bin/sable" + cp -a "$WORK/stage/share/." "$PKGROOT/usr/share/" + + PKGROOT="$PKGROOT" PKG_ARCH="$NFPM_ARCH" PKG_VERSION="$DEB_VERSION" PKG_RELEASE=1 nfpm pkg -f nfpm.webkit.yaml -p deb \ + -t "$OUT/deb/Sable-${VERSION}-linux-${ARCH}.deb" + PKGROOT="$PKGROOT" PKG_ARCH="$NFPM_ARCH" PKG_VERSION="$RPM_VERSION" PKG_RELEASE="$RPM_ITERATION" nfpm pkg -f nfpm.webkit.yaml -p rpm \ + -t "$OUT/rpm/Sable-${VERSION}-linux-${ARCH}.rpm" +else + echo "nfpm not found" >&2 + exit 1 +fi + +cp -f "$WORK/stage/share/applications/sable.desktop" "$APPDIR/sable.desktop" +cp -f src-tauri/icons/128x128.png "$APPDIR/sable.png" +cat > "$APPDIR/AppRun" <<'EOF' +#!/bin/sh +HERE="$(dirname "$(readlink -f "$0")")" +export LD_LIBRARY_PATH="$HERE/usr/bin${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +exec "$HERE/usr/bin/sable" "$@" +EOF +chmod 755 "$APPDIR/AppRun" + +APPIMAGE_EXTRACT_AND_RUN=1 "$APPIMAGETOOL_CMD" "$APPDIR" \ + "$OUT/appimage/Sable-${VERSION}-linux-${ARCH}.AppImage" + +echo "Packages in: $OUT" diff --git a/scripts/webkit/runtime.json b/scripts/webkit/runtime.json new file mode 100644 index 000000000..e20b52f6a --- /dev/null +++ b/scripts/webkit/runtime.json @@ -0,0 +1,7 @@ +{ + "$comment": "Portable WebKitGTK runtime embedded in wry Linux builds. Built by the webkitgtk-webrtc portable pipeline (glibc 2.39 floor, relocatable exec path). Re-pin `run` after each rebuild.", + "repository": "https://git.erwanleboucher.dev/eleboucher/webkitgtk-webrtc", + "run": 13112, + "artifact": "webkit2gtk-4.1-portable", + "version": "2.52.5" +} diff --git a/scripts/webkit/stage.sh b/scripts/webkit/stage.sh new file mode 100755 index 000000000..f68cf381b --- /dev/null +++ b/scripts/webkit/stage.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +#MISE description="Stage the WebKit runtime and desktop files for packaging" +# Usage: scripts/webkit/stage.sh [stage-dir] [display-name] [profile] +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT" + +STAGE="${1:-$ROOT/src-tauri/target/webkit-stage}" +DISPLAY_NAME="${2:-Sable}" +PROFILE="${3:-release}" + +RUNTIME="$STAGE/runtime" +SHARE="$STAGE/share" +rm -rf "$STAGE" +mkdir -p "$RUNTIME" "$SHARE/applications" + +bash scripts/webkit/copy-libs.sh "$PROFILE" "$RUNTIME" + +cat > "$SHARE/applications/sable.desktop" <