diff --git a/.github/workflows/tauri-build.yml b/.github/workflows/tauri-build.yml index 1137898322..8514a590d8 100644 --- a/.github/workflows/tauri-build.yml +++ b/.github/workflows/tauri-build.yml @@ -17,6 +17,11 @@ concurrency: group: tauri-build-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }} cancel-in-progress: ${{ github.ref == 'refs/heads/dev' }} +env: + CARGO_INCREMENTAL: 0 + CARGO_TERM_COLOR: always + NODE_OPTIONS: --max-old-space-size=8192 + jobs: setup-release: name: Resolve release target @@ -129,25 +134,16 @@ jobs: rust-targets: '' args: '' bundle_path: src-tauri/target/release/bundle - - name: Linux (CEF) - platform: ubuntu-22.04 - asset-platform: linux-x86_64 - rust-targets: '' - cef: true - bundle_path: src-tauri/target/release/bundle env: TAG: ${{ needs.setup-release.outputs.tag }} VERSION: ${{ needs.setup-release.outputs.version }} VITE_APP_VERSION: ${{ needs.setup-release.outputs.version }} - VITE_IS_RELEASE_TAG: ${{ needs.setup-release.outputs.nightly == 'true' && 'true' || '' }} + VITE_IS_RELEASE_TAG: ${{ needs.setup-release.outputs.nightly != 'true' }} 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 }} - CARGO_INCREMENTAL: 0 - CARGO_TERM_COLOR: always - NODE_OPTIONS: --max-old-space-size=8192 TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} steps: @@ -197,7 +193,6 @@ jobs: echo "TAURI_BUILD_ARGS=${ARGS# }" >> "$GITHUB_ENV" - name: Build desktop bundles - if: ${{ !matrix.cef }} shell: bash run: pnpm tauri build ${{ matrix.args }} $TAURI_BUILD_ARGS @@ -215,7 +210,6 @@ jobs: fi - name: Normalize desktop artifact names - if: ${{ !matrix.cef }} shell: bash env: ASSET_PLATFORM: ${{ matrix.asset-platform }} @@ -251,7 +245,6 @@ jobs: src-tauri/target/release/bundle/nsis/*.sig - name: Attach bundles to release - if: ${{ !matrix.cef }} shell: bash env: GH_TOKEN: ${{ github.token }} @@ -264,54 +257,6 @@ jobs: gh release upload "$TAG" "$path" --clobber done - # --- Linux: CEF runtime, packaged as deb / rpm / AppImage --- - - name: Build CEF binary - if: matrix.cef - shell: bash - run: pnpm tauri:cef build $TAURI_BUILD_ARGS - - - name: Build native packages - if: matrix.cef - shell: bash - run: mise run cef:package "$VERSION" - - - name: Sign the AppImage for the updater - if: ${{ matrix.cef && env.TAURI_SIGNING_PRIVATE_KEY != '' }} - shell: bash - run: | - for f in src-tauri/target/release/bundle/appimage/Sable-*-linux-x86_64.AppImage; do - [ -e "$f" ] || continue - pnpm tauri signer sign "$f" - done - - - name: Attest Linux bundles - if: matrix.cef - uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 - with: - subject-path: | - src-tauri/target/release/bundle/deb/*.deb - src-tauri/target/release/bundle/rpm/*.rpm - src-tauri/target/release/bundle/appimage/*.AppImage - src-tauri/target/release/bundle/appimage/*.sig - - - name: Attach native packages to release - if: matrix.cef - shell: bash - env: - GH_TOKEN: ${{ github.token }} - BUNDLE_PATH: ${{ matrix.bundle_path }} - run: | - BUNDLE="$BUNDLE_PATH" - test -d "$BUNDLE" || { echo "Bundle directory not found: $BUNDLE" >&2; exit 1; } - for f in "$BUNDLE"/deb/Sable-*-linux-x86_64.deb \ - "$BUNDLE"/rpm/Sable-*-linux-x86_64.rpm \ - "$BUNDLE"/appimage/Sable-*-linux-x86_64.AppImage \ - "$BUNDLE"/appimage/Sable-*-linux-x86_64.AppImage.sig; do - [ -e "$f" ] || continue - echo "Uploading $f" - gh release upload "$TAG" "$f" --clobber - done - android: name: Build Android needs: setup-release @@ -326,16 +271,13 @@ jobs: TAG: ${{ needs.setup-release.outputs.tag }} VERSION: ${{ needs.setup-release.outputs.version }} VITE_APP_VERSION: ${{ needs.setup-release.outputs.version }} - VITE_IS_RELEASE_TAG: ${{ needs.setup-release.outputs.nightly == 'true' && 'true' || '' }} + VITE_IS_RELEASE_TAG: ${{ needs.setup-release.outputs.nightly != 'true' }} 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 }} - CARGO_INCREMENTAL: 0 - CARGO_TERM_COLOR: always MISE_ENV: tauri - NODE_OPTIONS: --max-old-space-size=8192 # Matches the ABIs passed to `tauri android build` below. RUST_TARGETS: aarch64-linux-android armv7-linux-androideabi ANDROID_KEY_BASE64: ${{ secrets.ANDROID_KEY_BASE64 }} @@ -440,15 +382,12 @@ jobs: TAG: ${{ needs.setup-release.outputs.tag }} VERSION: ${{ needs.setup-release.outputs.version }} VITE_APP_VERSION: ${{ needs.setup-release.outputs.version }} - VITE_IS_RELEASE_TAG: ${{ needs.setup-release.outputs.nightly == 'true' && 'true' || '' }} + VITE_IS_RELEASE_TAG: ${{ needs.setup-release.outputs.nightly != 'true' }} 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 }} - CARGO_INCREMENTAL: 0 - CARGO_TERM_COLOR: always - NODE_OPTIONS: --max-old-space-size=8192 RUST_TARGETS: aarch64-apple-ios steps: - name: Checkout repository @@ -544,7 +483,7 @@ jobs: updater-manifest: name: Publish updater manifest - needs: [setup-release, build] + needs: [setup-release, build, linux] runs-on: ubuntu-latest timeout-minutes: 10 permissions: @@ -556,7 +495,7 @@ jobs: TAG: ${{ needs.setup-release.outputs.tag }} VERSION: ${{ needs.setup-release.outputs.version }} VITE_APP_VERSION: ${{ needs.setup-release.outputs.version }} - VITE_IS_RELEASE_TAG: ${{ needs.setup-release.outputs.nightly == 'true' && 'true' || '' }} + VITE_IS_RELEASE_TAG: ${{ needs.setup-release.outputs.nightly != 'true' }} REPO: ${{ github.repository }} STARTED_AT: ${{ needs.setup-release.outputs.started_at }} steps: @@ -628,3 +567,231 @@ jobs: echo "Removing superseded nightly asset: $name" gh release delete-asset nightly "$name" --yes done + + # Nightlies use `--snapshot`, which packages without publishing. + linux: + name: Build Linux (CEF) + needs: setup-release + runs-on: ubuntu-22.04 + timeout-minutes: 60 + permissions: + contents: write + id-token: write + attestations: write + artifact-metadata: write + env: + TAG: ${{ needs.setup-release.outputs.tag }} + VERSION: ${{ needs.setup-release.outputs.version }} + VITE_APP_VERSION: ${{ needs.setup-release.outputs.version }} + VITE_IS_RELEASE_TAG: ${{ needs.setup-release.outputs.nightly != 'true' }} + 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 }} + 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 }} + # GoReleaser reads the version from git history. + fetch-depth: 0 + persist-credentials: false + + - name: Setup app + uses: ./.github/actions/setup + with: + tauri: 'true' + + - name: Cache Rust build + uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + with: + workspaces: src-tauri + + - 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" + + # Not in [bootstrap.packages]: snapcraft ships only as a snap. + - name: Install snapcraft + if: ${{ needs.setup-release.outputs.nightly != 'true' }} + run: sudo snap install snapcraft --classic + + - name: Trust the AUR host key + if: ${{ needs.setup-release.outputs.nightly != 'true' }} + run: | + mkdir -p ~/.ssh + ssh-keyscan -t ed25519 aur.archlinux.org >> ~/.ssh/known_hosts + + - name: Resolve GoReleaser args + id: gr + shell: bash + run: | + if [ "$IS_NIGHTLY" = "true" ]; then + echo 'args=release --clean --snapshot --skip=snapcraft,aur' >> "$GITHUB_OUTPUT" + else + echo 'args=release --clean' >> "$GITHUB_OUTPUT" + fi + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 + with: + # The rust builder needs >= 2.5. + version: '~> v2' + args: ${{ steps.gr.outputs.args }} + env: + GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN || github.token }} + AUR_KEY: ${{ secrets.AUR_KEY }} + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + + - name: Build the AppImage + shell: bash + run: | + DISPLAY_NAME=Sable + [ "$IS_NIGHTLY" = "true" ] && DISPLAY_NAME="Sable Nightly" + mise run cef:package "$VERSION" \ + src-tauri/target/x86_64-unknown-linux-gnu/release/sable "$DISPLAY_NAME" + + - 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-x86_64.AppImage; do + [ -e "$f" ] || continue + pnpm tauri signer sign "$f" + done + + - name: Attest Linux bundles + uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 + with: + subject-path: | + .goreleaser-dist/*.deb + .goreleaser-dist/*.rpm + .goreleaser-dist/*.pkg.tar.zst + src-tauri/target/release/bundle/appimage/*.AppImage + src-tauri/target/release/bundle/appimage/*.sig + + - name: Attach Linux packages to release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + for f in .goreleaser-dist/Sable-*.deb \ + .goreleaser-dist/Sable-*.rpm \ + .goreleaser-dist/Sable-*.pkg.tar.zst \ + src-tauri/target/release/bundle/appimage/Sable-*-linux-x86_64.AppImage \ + src-tauri/target/release/bundle/appimage/Sable-*-linux-x86_64.AppImage.sig; do + [ -e "$f" ] || continue + echo "Uploading $f" + gh release upload "$TAG" "$f" --clobber + done + + - name: Upload .deb as workflow artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: sable-linux-deb + path: .goreleaser-dist/Sable-*.deb + retention-days: 1 + + distribute-nightly-aur: + name: Publish nightly to AUR + needs: [setup-release, linux] + runs-on: ubuntu-latest + timeout-minutes: 10 + if: ${{ needs.setup-release.outputs.nightly == 'true' }} + permissions: + contents: read + env: + TAG: ${{ needs.setup-release.outputs.tag }} + VERSION: ${{ needs.setup-release.outputs.version }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Download .deb artifact and compute checksum + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: sable-linux-deb + path: deb-artifact + + - name: Compute checksum + shell: bash + run: | + set -- deb-artifact/Sable-*-linux-x86_64.deb + [ "$#" -eq 1 ] && [ -e "$1" ] || { echo "Expected exactly one .deb, got $*" >&2; exit 1; } + echo "SHA256=$(sha256sum "$1" | awk '{print $1}')" >> "$GITHUB_ENV" + + - name: Push to AUR sable-nightly-bin + shell: bash + env: + AUR_KEY: ${{ secrets.AUR_KEY }} + run: | + if [ -z "${AUR_KEY:-}" ]; then + echo "::notice::AUR_KEY is not set; skipping the AUR publish." + exit 0 + fi + mkdir -p ~/.ssh + printf '%s\n' "$AUR_KEY" > ~/.ssh/aur_key + chmod 600 ~/.ssh/aur_key + ssh-keyscan -t ed25519 aur.archlinux.org >> ~/.ssh/known_hosts + export GIT_SSH_COMMAND="ssh -i $HOME/.ssh/aur_key -o IdentitiesOnly=yes" + + git clone "ssh://aur@aur.archlinux.org/sable-nightly-bin.git" aur-repo + cp packaging/aur/sable-nightly-bin.PKGBUILD aur-repo/PKGBUILD + cp packaging/aur/sable-nightly-bin.install aur-repo/sable-nightly-bin.install + + cd aur-repo + # pkgver forbids '-'; _relver keeps the asset URL's form. + sed -i \ + -e "s/^pkgver=.*/pkgver=${VERSION//-/.}/" \ + -e "s/^pkgrel=.*/pkgrel=1/" \ + -e "s/^_relver=.*/_relver=${VERSION}/" \ + -e "s/^sha256sums_x86_64=.*/sha256sums_x86_64=('${SHA256}')/" \ + PKGBUILD + + # makepkg refuses to run as root. + docker run --rm -v "$PWD:/src:ro" archlinux:base-devel bash -c ' + useradd -m build + cp /src/PKGBUILD /home/build/ + su build -c "cd /home/build && makepkg --printsrcinfo" + ' > .SRCINFO + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add PKGBUILD sable-nightly-bin.install .SRCINFO + if git diff --cached --quiet; then + echo "Nothing changed; skipping the push." + exit 0 + fi + git commit -m "nightly ${VERSION}" + git push + + distribute-obtainium: + name: Generate Obtainium import file + needs: [setup-release, android] + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: write + env: + TAG: ${{ needs.setup-release.outputs.tag }} + VERSION: ${{ needs.setup-release.outputs.version }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Generate obtainium.json + shell: bash + run: node scripts/obtainium-generate.js "$VERSION" "$TAG" obtainium.json + + - name: Upload to release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: gh release upload "$TAG" obtainium.json --clobber diff --git a/.gitignore b/.gitignore index 6b140bd38a..97eb818375 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ experiment dist +.goreleaser-dist coverage node_modules devAssets diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000000..34d8b0cdbf --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,204 @@ +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# Linux packaging for the CEF build. Tauri builds the macOS/Windows bundles; +# GoReleaser OSS can't import pre-built artifacts (`builder: prebuilt` is Pro). +# +# Secrets: AUR_KEY, SNAPCRAFT_STORE_CREDENTIALS + +version: 2 + +project_name: sable + +# Vite owns ./dist. +dist: .goreleaser-dist + +before: + hooks: + - pnpm install --frozen-lockfile + # cargo skips tauri.conf.json's beforeBuildCommand. + - pnpm build + +builds: + - id: sable + builder: rust + dir: src-tauri + tool: cargo + command: build + targets: [x86_64-unknown-linux-gnu] + flags: + - --release + - --no-default-features + - --features + - cef,updater + hooks: + post: bash scripts/cef/stage.sh + +archives: + - id: sable + formats: [tar.gz] + name_template: 'Sable-{{ .Version }}-linux-x86_64' + files: + - LICENSE + - src: src-tauri/target/cef-stage/runtime + dst: . + - src: src-tauri/target/cef-stage/share + dst: . + +nfpms: + - package_name: sable + file_name_template: 'Sable-{{ .Version }}-linux-x86_64' + homepage: https://sable.moe + maintainer: Sable Maintainers + description: Sable, a Matrix client + license: AGPL-3.0-or-later + section: net + formats: [deb, rpm, archlinux] + # Beside the CEF runtime, for build.rs's rpath=$ORIGIN. + bindir: /opt/sable + provides: [sable] + contents: + - src: src-tauri/target/cef-stage/runtime/ + dst: /opt/sable + type: tree + - src: src-tauri/target/cef-stage/share/ + dst: /usr/share + type: tree + - src: /opt/sable/sable + dst: /usr/bin/sable + type: symlink + scripts: + postinstall: packaging/linux/postinstall.sh + postremove: packaging/linux/postinstall.sh + overrides: + deb: + dependencies: + - libgtk-3-0 + - libnss3 + - libnspr4 + - libgbm1 + - libdrm2 + - libxkbcommon0 + - libxss1 + - libasound2 + - libcups2 + - xdg-utils + - libayatana-appindicator3-1 + rpm: + dependencies: + - gtk3 + - nss + - nspr + - mesa-libgbm + - libdrm + - libxkbcommon + - libXScrnSaver + - alsa-lib + - cups-libs + - xdg-utils + - libayatana-appindicator3.so.1()(64bit) + archlinux: + dependencies: + - gtk3 + - nss + - alsa-lib + - libcups + - libdrm + - mesa + - libxkbcommon + - libxss + - at-spi2-core + - dbus + - xdg-utils + - libayatana-appindicator + - hicolor-icon-theme + - desktop-file-utils + +aurs: + - name: sable-bin + homepage: https://sable.moe + description: Sable, a Matrix client + maintainers: + - Sable Maintainers + license: AGPL-3.0-or-later + private_key: '{{ .Env.AUR_KEY }}' + git_url: ssh://aur@aur.archlinux.org/sable-bin.git + provides: [sable] + conflicts: [sable, sable-nightly-bin] + depends: + - gtk3 + - nss + - alsa-lib + - libcups + - libdrm + - mesa + - libxkbcommon + - libxss + - at-spi2-core + - dbus + - xdg-utils + - libayatana-appindicator + - hicolor-icon-theme + - desktop-file-utils + package: |- + install -Dm755 sable "${pkgdir}/opt/sable/sable" + cp -a runtime/. "${pkgdir}/opt/sable/" + install -dm755 "${pkgdir}/usr/share" "${pkgdir}/usr/bin" + cp -a share/. "${pkgdir}/usr/share/" + ln -s /opt/sable/sable "${pkgdir}/usr/bin/sable" + install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" + commit_author: + name: github-actions[bot] + email: github-actions[bot]@users.noreply.github.com + +snapcrafts: + - name: sable + title: Sable + summary: Sable, a Matrix client + description: | + Sable is a cross-platform Matrix client built with Tauri, offering a + native desktop experience with end-to-end encryption support. + icon: src-tauri/icons/128x128@2x.png + license: AGPL-3.0-or-later + grade: stable + confinement: strict + base: core22 + publish: true + extra_files: + # Binaries land at the prime root. + - source: src-tauri/target/cef-stage/runtime + destination: . + - source: src-tauri/target/cef-stage/share/applications/sable.desktop + destination: meta/gui/sable.desktop + - source: src-tauri/icons/128x128@2x.png + destination: meta/gui/icon.png + apps: + sable: + command: sable + desktop: meta/gui/sable.desktop + extensions: [gnome] + plugs: + - network + - network-bind + - home + - removable-media + - browser-support + - audio-playback + - audio-record + - camera + - password-manager-service + +checksum: + name_template: 'Sable-{{ .Version }}-linux-checksums.txt' + algorithm: sha256 + +# Nightlies run `release --snapshot`; GoReleaser's `nightly` pipe is Pro-only. +snapshot: + version_template: '{{ .Env.VERSION }}' + +release: + github: + owner: SableClient + name: Sable + # Knope writes the release notes. + mode: keep-existing + draft: false + prerelease: auto diff --git a/README.md b/README.md index 57e39c72df..fb562c3ec8 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,20 @@ The stable web app is available at [app.sable.moe](https://app.sable.moe/) and t You can also download our desktop app for Windows and Linux from [releases](https://github.com/SableClient/Sable/releases/latest). Release artifacts include build attestations, and desktop installations update automatically. +### Desktop (Linux / macOS / Windows) + +Download +  +AUR +  +Snap Store + +### Android + +Download APK +  +Add to Obtainium + ## iOS (AltStore / SideStore) Sable iOS builds are distributed as unsigned IPAs through [AltStore](https://altstore.io) and [SideStore](https://sidestore.io). Each release publishes both the IPA and an `altstore-source.json` manifest — stable builds to the [latest GitHub release](https://github.com/SableClient/Sable/releases/latest), nightly builds to the [`nightly` GitHub release](https://github.com/SableClient/Sable/releases/tag/nightly). diff --git a/mise.tauri.toml b/mise.tauri.toml index 116c3f7294..24a808864a 100644 --- a/mise.tauri.toml +++ b/mise.tauri.toml @@ -1,5 +1,5 @@ # Tauri-specific dev tools and system dependencies. -# Loaded via `MISE_ENV=tauri` (set by `mise run setup:tauri` or CI). +# Loaded via `MISE_ENV=tauri` (set by the `tauri:setup` task's env, or CI). [env] ANDROID_NDK_VERSION = "29.0.14206865" diff --git a/mise.toml b/mise.toml index 1dc5170879..ea9524de1f 100644 --- a/mise.toml +++ b/mise.toml @@ -57,7 +57,7 @@ tools = { "cargo:knope" = "0.23.0", cargo-binstall = "1.20.1" } [tasks."knope:document-change"] description = "Document a change with knope" -alias = ["doc", "doc"] +alias = ["doc", "dc"] run = { task = "knope", args = ["document-change"] } [tasks."tauri:setup"] @@ -93,7 +93,6 @@ run = [ "winget install --id Microsoft.EdgeWebView2Runtime --source winget", ] - [tasks."cargo:lint"] description = "Check Rust formatting and Clippy lints" alias = ["cl", "cargo-lint"] diff --git a/nfpm.yaml b/nfpm.yaml deleted file mode 100644 index e6b78751e0..0000000000 --- a/nfpm.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# yaml-language-server: $schema=https://nfpm.goreleaser.com/schema.json - -name: sable -arch: amd64 -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 - -overrides: - deb: - depends: - - libgtk-3-0 - - libnss3 - - libnspr4 - - libgbm1 - - libdrm2 - - libxkbcommon0 - - libasound2 - - libcups2 - - xdg-utils - - xwayland - - libayatana-appindicator3-1 - rpm: - depends: - - gtk3 - - nss - - nspr - - mesa-libgbm - - libdrm - - libxkbcommon - - alsa-lib - - cups-libs - - xdg-utils - - xorg-x11-server-Xwayland - - libayatana-appindicator3.so.1()(64bit) diff --git a/packaging/aur/sable-nightly-bin.PKGBUILD b/packaging/aur/sable-nightly-bin.PKGBUILD new file mode 100644 index 0000000000..316bdf8655 --- /dev/null +++ b/packaging/aur/sable-nightly-bin.PKGBUILD @@ -0,0 +1,45 @@ +# Maintainer: Sable Maintainers + +pkgname=sable-nightly-bin +# pkgver mirrors the release version with '-' replaced by '.', so each nightly +# sorts above the last. _relver keeps the original form for the asset URL. +pkgver=1.20.1.nightly.2607242130 +pkgrel=1 +_relver=1.20.1-nightly.2607242130 +pkgdesc="An almost stable Matrix client (nightly builds)" +arch=('x86_64') +url="https://github.com/SableClient/Sable" +license=('AGPL-3.0-or-later') +# The bundled CEF runtime needs Chromium's system libraries, not webkit2gtk. +depends=( + 'gtk3' + 'nss' + 'alsa-lib' + 'libcups' + 'libdrm' + 'mesa' + 'libxkbcommon' + 'libxss' + 'libxcomposite' + 'libxdamage' + 'libxrandr' + 'at-spi2-core' + 'dbus' + 'libayatana-appindicator' + 'hicolor-icon-theme' + 'desktop-file-utils' + 'xdg-utils' +) +provides=('sable') +conflicts=('sable' 'sable-bin') +options=('!strip' '!debug') +install=${pkgname}.install +source_x86_64=("${pkgname}-${pkgver}.deb::${url}/releases/download/nightly/Sable-${_relver}-linux-x86_64.deb") +sha256sums_x86_64=('0000000000000000000000000000000000000000000000000000000000000000') + +package() { + # bsdtar reads whichever compression nfpm used for data.tar.* + bsdtar -O -xf "${srcdir}/${pkgname}-${pkgver}.deb" 'data.tar*' \ + | bsdtar -xp -C "${pkgdir}" + find "${pkgdir}" -type d -exec chmod 755 {} + +} diff --git a/packaging/aur/sable-nightly-bin.install b/packaging/aur/sable-nightly-bin.install new file mode 100644 index 0000000000..71627fa642 --- /dev/null +++ b/packaging/aur/sable-nightly-bin.install @@ -0,0 +1,12 @@ +post_install() { + gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor + update-desktop-database -q +} + +post_upgrade() { + post_install +} + +post_remove() { + post_install +} diff --git a/packaging/linux/postinstall.sh b/packaging/linux/postinstall.sh new file mode 100644 index 0000000000..44073ed5b2 --- /dev/null +++ b/packaging/linux/postinstall.sh @@ -0,0 +1,9 @@ +#!/bin/sh +set -e + +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor || true +fi +if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database -q || true +fi diff --git a/scripts/cef/copy-libs.sh b/scripts/cef/copy-libs.sh index fe776923e6..dd9cd6c423 100755 --- a/scripts/cef/copy-libs.sh +++ b/scripts/cef/copy-libs.sh @@ -12,12 +12,13 @@ PROFILE="${1:-debug}" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" DEST="${2:-$ROOT/src-tauri/target/$PROFILE}" +# --target moves build-script output under target//. CEF_DIR="$( - find "$ROOT/src-tauri/target/$PROFILE/build" \ - -type d -name cef_linux_x86_64 -print -quit 2>/dev/null || true + find "$ROOT/src-tauri/target" -type d -name cef_linux_x86_64 \ + -path "*/$PROFILE/build/*" -print -quit 2>/dev/null || true )" if [ -z "$CEF_DIR" ]; then - echo "❌ CEF dist not found under target/$PROFILE/build — build with --features cef first." >&2 + echo "❌ CEF dist not found under target/**/$PROFILE/build — build with --features cef first." >&2 exit 1 fi @@ -38,10 +39,5 @@ done mkdir -p "$DEST/locales" cp -f "$CEF_DIR"/locales/en-US.pak "$DEST/locales/" 2>/dev/null || true -# The setuid sandbox helper only works when installed as root (deb/rpm). In a -# nosuid context such as an AppImage, drop chrome-sandbox and let Chromium use -# the user-namespace sandbox instead. -if cp -f "$CEF_DIR"/chrome-sandbox "$DEST/" 2>/dev/null; then - chmod 4755 "$DEST/chrome-sandbox" 2>/dev/null || true -fi +# chrome-sandbox is not shipped: AppImages and snaps mount nosuid. echo "✅ done." diff --git a/scripts/cef/package.sh b/scripts/cef/package.sh index d1be2d9ab3..e7ddc1d7ee 100755 --- a/scripts/cef/package.sh +++ b/scripts/cef/package.sh @@ -1,54 +1,60 @@ #!/usr/bin/env bash -#MISE description="Package CEF build into deb/rpm/AppImage" -#MISE tools={nfpm="2.47.0", "github:AppImage/appimagetool" = {version = "1.9.1", matching = ".AppImage"}} -# Package the Linux CEF build into deb/rpm/AppImage. Used by CI and locally. -# Run after the binary is built (pnpm tauri:cef build). -# Usage: scripts/cef/package.sh [version] +#MISE description="Package the CEF build into an AppImage" +#MISE tools={"github:AppImage/appimagetool" = {version = "1.9.1", matching = ".AppImage"}} +# deb/rpm/archlinux/snap come from GoReleaser. +# Usage: scripts/cef/package.sh [version] [binary-path] [display-name] 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#*-}" - # Debian sorts ~ before the final version. RPM uses the release field so its - # Version remains hyphen-free and a 0.* prerelease sorts before release 1. - DEB_VERSION="${BASE_VERSION}~${PRERELEASE}" - RPM_VERSION="$BASE_VERSION" - RPM_ITERATION="0.${PRERELEASE}" -fi + STAGE="$ROOT/src-tauri/target/release" OUT="$STAGE/bundle" WORK="$STAGE/cef-pkg" -if [ -x "$STAGE/Sable Nightly" ]; then - BIN_NAME="Sable Nightly" - DISPLAY_NAME="Sable Nightly" -elif [ -x "$STAGE/Sable" ]; then - BIN_NAME="Sable" - DISPLAY_NAME="Sable" -elif [ -x "$STAGE/sable" ]; then - BIN_NAME="sable" - DISPLAY_NAME="Sable" +BIN_PATH="${2:-}" +DISPLAY_NAME="${3:-}" +if [ -z "$BIN_PATH" ]; then + for candidate in "$STAGE/Sable Nightly" "$STAGE/Sable" "$STAGE/sable" \ + "$ROOT/src-tauri/target/x86_64-unknown-linux-gnu/release/sable"; do + [ -x "$candidate" ] || continue + BIN_PATH="$candidate" + break + done +fi +[ -n "$BIN_PATH" ] && [ -x "$BIN_PATH" ] || { + echo "no CEF binary found; build it first (pnpm tauri:cef build)" >&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 "missing $STAGE/Sable Nightly, Sable, or sable; build it first (pnpm tauri:cef build)" >&2 + echo "appimagetool not found" >&2 exit 1 fi rm -rf "$WORK" -mkdir -p "$WORK" "$OUT/deb" "$OUT/rpm" "$OUT/appimage" +mkdir -p "$OUT/appimage" -stage_runtime() { - local dest="$1" - mkdir -p "$dest" - cp "$STAGE/$BIN_NAME" "$dest/sable" - bash scripts/cef/copy-libs.sh release "$dest" -} +bash scripts/cef/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 "$BIN_PATH" "$APPDIR/usr/bin/sable" +chmod 755 "$APPDIR/usr/bin/sable" # Bundle the system-tray libraries (Tauri's linuxdeploy path normally does this, # which the CEF build bypasses). Bundle libayatana-appindicator3 plus its @@ -56,7 +62,6 @@ stage_runtime() { # are left to the system, matching linuxdeploy-plugin-appindicator. stage_appindicator() { local dest="$1" main dep - mkdir -p "$dest" # awk reads to EOF (no early exit) so ldconfig never gets SIGPIPE under pipefail. 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)" @@ -73,79 +78,19 @@ stage_appindicator() { fi done } +stage_appindicator "$APPDIR/usr/bin" -write_desktop() { - cat > "$1" </dev/null 2>&1; then - PKGROOT="$WORK/pkgroot" - export PKGROOT - stage_runtime "$PKGROOT/opt/sable" - mkdir -p "$PKGROOT/usr/bin" "$PKGROOT/usr/share/applications" - cat > "$PKGROOT/usr/bin/sable" <<'EOF' -#!/bin/sh -exec /opt/sable/sable "$@" -EOF - chmod 755 "$PKGROOT/usr/bin/sable" - write_desktop "$PKGROOT/usr/share/applications/sable.desktop" - for size in 32x32 64x64 128x128; do - mkdir -p "$PKGROOT/usr/share/icons/hicolor/${size}/apps" - cp "src-tauri/icons/${size}.png" \ - "$PKGROOT/usr/share/icons/hicolor/${size}/apps/sable.png" - done - mkdir -p "$PKGROOT/usr/share/icons/hicolor/256x256/apps" - cp "src-tauri/icons/128x128@2x.png" \ - "$PKGROOT/usr/share/icons/hicolor/256x256/apps/sable.png" - - PKG_VERSION="$DEB_VERSION" PKG_RELEASE=1 nfpm pkg -f "$CONFIG" -p deb \ - -t "$OUT/deb/Sable-${VERSION}-linux-x86_64.deb" - - PKG_VERSION="$RPM_VERSION" PKG_RELEASE="$RPM_ITERATION" nfpm pkg -f "$CONFIG" -p rpm \ - -t "$OUT/rpm/Sable-${VERSION}-linux-x86_64.rpm" -else - echo "nfpm not found; skipping deb/rpm" -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" -fi - -if [ -n "$APPIMAGETOOL_CMD" ]; then - APPDIR="$WORK/Sable.AppDir" - stage_runtime "$APPDIR/usr/bin" - stage_appindicator "$APPDIR/usr/bin" - # nosuid AppImage mount: drop setuid chrome-sandbox, use the namespace sandbox. - rm -f "$APPDIR/usr/bin/chrome-sandbox" - write_desktop "$APPDIR/sable.desktop" - cp src-tauri/icons/128x128.png "$APPDIR/sable.png" - cat > "$APPDIR/AppRun" <<'EOF' +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" +chmod 755 "$APPDIR/AppRun" - APPIMAGE_EXTRACT_AND_RUN=1 ARCH=x86_64 "$APPIMAGETOOL_CMD" "$APPDIR" \ - "$OUT/appimage/Sable-${VERSION}-linux-x86_64.AppImage" -else - echo "appimagetool not found; skipping AppImage" >&2 -fi +APPIMAGE_EXTRACT_AND_RUN=1 ARCH=x86_64 "$APPIMAGETOOL_CMD" "$APPDIR" \ + "$OUT/appimage/Sable-${VERSION}-linux-x86_64.AppImage" -echo "Packages in: $OUT" +echo "AppImage in: $OUT/appimage" diff --git a/scripts/cef/stage.sh b/scripts/cef/stage.sh new file mode 100755 index 0000000000..f1aaad2e2e --- /dev/null +++ b/scripts/cef/stage.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +#MISE description="Stage the CEF runtime and desktop files for packaging" +# Usage: scripts/cef/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/cef-stage}" +DISPLAY_NAME="${2:-Sable}" +PROFILE="${3:-release}" + +RUNTIME="$STAGE/runtime" +SHARE="$STAGE/share" +rm -rf "$STAGE" +mkdir -p "$RUNTIME" "$SHARE/applications" + +bash scripts/cef/copy-libs.sh "$PROFILE" "$RUNTIME" + +cat > "$SHARE/applications/sable.desktop" < [tag] [output-path]'); + process.exit(1); +} + +const GITHUB_REPO = 'SableClient/Sable'; +const APK_NAME = `Sable-${version}-android-universal.apk`; +const APK_URL = `https://github.com/${GITHUB_REPO}/releases/download/${tag}/${APK_NAME}`; +const isPrerelease = tag === 'nightly' || /-/.test(version); + +const config = { + schemaVersion: 2, + exportedAt: new Date().toISOString(), + appVersion: '1.0.0', + apps: [ + { + id: 'moe.sable.client', + url: `https://github.com/${GITHUB_REPO}`, + author: 'SableClient', + name: 'Sable', + installedVersion: null, + latestVersion: version, + apkUrls: JSON.stringify([[APK_NAME, APK_URL]]), + otherAssetUrls: '[]', + preferredApkIndex: 0, + additionalSettings: JSON.stringify({ + includePrereleases: isPrerelease, + fallbackToOlderReleases: true, + filterReleaseTitlesByRegEx: '', + filterReleaseNotesByRegEx: '', + verifyLatestTag: false, + sortMethodChoice: 'date', + useLatestAssetDateAsReleaseDate: false, + releaseTitleAsVersion: false, + trackOnly: false, + versionExtractionRegEx: '', + matchGroupToUse: '', + versionDetection: true, + releaseDateAsVersion: false, + useVersionCodeAsOSVersion: false, + apkFilterRegEx: '', + invertAPKFilter: false, + autoApkFilterByArch: true, + appName: '', + appAuthor: '', + shizukuPretendToBeGooglePlay: false, + allowInsecure: false, + exemptFromBackgroundUpdates: false, + skipUpdateNotifications: false, + about: 'An almost stable Matrix client', + refreshBeforeDownload: false, + }), + lastUpdateCheck: null, + pinned: false, + categories: ['Communication'], + releaseDate: null, + changeLog: null, + overrideSource: 'GitHub', + allowIdChange: false, + pendingRepoRenameUrl: null, + }, + ], + settings: { + categories: { + Communication: 4290362623, + }, + groupByCategory: true, + }, +}; + +writeFileSync(resolve(outputPath), JSON.stringify(config, null, 2) + '\n'); +console.log(`Generated ${outputPath}`);