diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml new file mode 100644 index 0000000..425e226 --- /dev/null +++ b/.github/workflows/flatpak.yml @@ -0,0 +1,163 @@ +name: Flatpak + +on: + push: + branches: [main] + paths: ['flatpak/**', 'src/**', 'package.json', 'package-lock.json', 'electron-builder.yml', '.github/workflows/flatpak.yml'] + pull_request: + paths: ['flatpak/**', 'src/**', 'package.json', 'package-lock.json', 'electron-builder.yml', '.github/workflows/flatpak.yml'] + workflow_dispatch: + +concurrency: + group: flatpak-${{ github.ref }} + cancel-in-progress: true + +env: + MANIFEST: flatpak/com.andersonlaverde.slacky.yml + APP_ID: com.andersonlaverde.slacky + +jobs: + metadata: + name: Validate metadata + runs-on: ubuntu-24.04-arm + steps: + - uses: actions/checkout@v7 + + - name: Install validators + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends appstream desktop-file-utils + + # --no-net keeps this to structural validation. The screenshot URL is + # checked separately below, because it is a Flathub submission + # requirement rather than something that blocks a build. + - name: Validate AppStream metainfo + run: appstreamcli validate --explain --no-net flatpak/${{ env.APP_ID }}.metainfo.xml + + - name: Check screenshots resolve + run: | + urls=$(grep -oP '(?<=)[^<]+' flatpak/${{ env.APP_ID }}.metainfo.xml) + missing=0 + for url in $urls; do + if curl -sSfLI --max-time 30 "$url" >/dev/null 2>&1; then + echo "ok $url" + else + echo "::warning file=flatpak/${{ env.APP_ID }}.metainfo.xml::Screenshot $url does not resolve. Flathub rejects submissions with unreachable screenshots." + missing=1 + fi + done + if [ "$missing" = 1 ]; then + echo "Screenshots are still outstanding; see the checklist in flatpak/README.md." + fi + + - name: Validate desktop entry + run: desktop-file-validate flatpak/${{ env.APP_ID }}.desktop + + build: + name: Build flatpak (arm64) + runs-on: ubuntu-24.04-arm + timeout-minutes: 90 + steps: + - uses: actions/checkout@v7 + + # The build sandbox has no network, so every npm tarball and the Electron + # binary have to be declared as sources up front. See flatpak/README.md. + - name: Generate offline npm sources + run: | + python3 -m venv .venv + .venv/bin/pip install --quiet PyYAML \ + 'flatpak-node-generator @ git+https://github.com/flatpak/flatpak-builder-tools.git#subdirectory=node' + .venv/bin/flatpak-node-generator npm package-lock.json -o flatpak/generated-sources.json + + # The committed manifest builds a published tag, which is what Flathub + # needs but would make CI test the last release instead of this commit. + # Swap that one source for the checked-out tree. + - name: Point the manifest at the checked-out tree + run: | + .venv/bin/python - <<'PY' + import sys, os, yaml + path = os.environ['MANIFEST'] + with open(path) as f: + manifest = yaml.safe_load(f) + local = {'type': 'dir', 'path': '..', + 'skip': ['.git', '.venv', 'node_modules', 'dist', 'build-dir']} + swapped = 0 + # Every module, not a hardcoded index: the app module is not + # necessarily first, and silently rewriting the wrong one means CI + # builds the last release instead of the commit under test. + for module in manifest['modules']: + sources = module.get('sources') + if not sources: + continue + for i, source in enumerate(sources): + if isinstance(source, dict) and source.get('type') == 'git': + sources[i] = local + swapped += 1 + if swapped != 1: + sys.exit(f'expected exactly one git source to swap, found {swapped}') + with open(path, 'w') as f: + yaml.safe_dump(manifest, f, sort_keys=False) + PY + cat "$MANIFEST" + + - name: Install flatpak-builder + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends flatpak flatpak-builder elfutils + + # ~1.5 GB of runtimes, and dl.flathub.org times out often enough to + # matter. Exact key only: a half-restored ostree repo is worse than none, + # and the install step below repairs anything the cache got wrong. + - name: Cache flatpak runtimes + uses: actions/cache@v6 + with: + path: ~/.local/share/flatpak + key: flatpak-runtimes-24.08-node24-electron2 + + - name: Install runtimes + run: | + # dl.flathub.org has served truncated objects and 503s for the Node + # SDK extension. Back off far enough to ride out a bad spell rather + # than hammering a CDN that is already unwell. + retry() { + attempts=4 + delay=30 + for attempt in $(seq $attempts); do + "$@" && return 0 + if [ "$attempt" -eq "$attempts" ]; then + echo "::error::flathub failed $attempts times, giving up" + return 1 + fi + echo "::warning::flathub attempt $attempt failed, retrying in ${delay}s" + sleep $delay + delay=$((delay * 2)) + done + } + retry flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo + retry flatpak install --user -y --noninteractive flathub \ + org.freedesktop.Platform//24.08 \ + org.freedesktop.Sdk//24.08 \ + org.freedesktop.Sdk.Extension.node24//24.08 \ + org.electronjs.Electron2.BaseApp//24.08 + + - name: Cache build downloads + uses: actions/cache@v6 + with: + path: .flatpak-builder/downloads + key: flatpak-downloads-${{ hashFiles('package-lock.json') }} + restore-keys: flatpak-downloads- + + - name: Build + run: | + flatpak-builder --user --force-clean --disable-rofiles-fuse \ + --repo=repo build-dir "$MANIFEST" + + - name: Bundle + run: | + flatpak build-bundle repo slacky.flatpak "$APP_ID" --runtime-repo=https://dl.flathub.org/repo/flathub.flatpakrepo + + - uses: actions/upload-artifact@v7 + with: + name: slacky-flatpak-arm64 + path: slacky.flatpak + if-no-files-found: error diff --git a/.gitignore b/.gitignore index 7fd582d..f4486c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ node_modules/ dist/* .DS_Store + +# flatpak build artifacts +flatpak/generated-sources.json +flatpak-node/ +flatpak-node-generator.py +.flatpak-builder/ +build-dir/ diff --git a/electron-builder.yml b/electron-builder.yml index 2b5bb6e..ac5df49 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -17,6 +17,9 @@ protocols: linux: category: Network synopsis: Slack client for Linux arm64 systems + # Pinned so the binary inside the unpacked build has a predictable name; the + # flatpak wrapper in flatpak/slacky.sh execs /app/main/slacky. + executableName: slacky target: - target: AppImage arch: diff --git a/flatpak/README.md b/flatpak/README.md new file mode 100644 index 0000000..80b2f14 --- /dev/null +++ b/flatpak/README.md @@ -0,0 +1,103 @@ +# Flatpak / Flathub packaging + +Files here build Slacky as a flatpak and prepare the Flathub submission for +[#10](https://github.com/andirsun/Slacky/issues/10). + +| File | Purpose | +| --- | --- | +| `com.andersonlaverde.slacky.yml` | flatpak-builder manifest | +| `com.andersonlaverde.slacky.metainfo.xml` | AppStream metadata (mandatory on Flathub) | +| `com.andersonlaverde.slacky.desktop` | Desktop entry | +| `slacky.sh` | Launcher, wraps the app in zypak | + +## Two things that are not obvious + +`electronDist` is passed explicitly because flatpak-node-generator writes the +Electron zip flat into `$XDG_CACHE_HOME/electron`, while `@electron/get` looks +for it under a `sha256`-of-the-URL subdirectory. The cached zip is therefore +invisible to electron-builder, which then tries to reach github.com and fails +in the network-less build sandbox. Unpacking it and passing `electronDist` +sidesteps the cache layout entirely. + +`npm ci`, not `npm install`: install re-resolves the dependency tree and +reaches for packages outside the lockfile, which are by definition absent from +the generated offline sources (`ENOTCACHED`). + +## Why this does not use electron-builder's flatpak target + +electron-builder's `flatpak` target shells out to `flatpak-builder` itself. The +first attempt at this manifest called `npm run pack -- --linux flatpak` from +inside `build-commands`, which runs flatpak-builder inside a flatpak build — that +is what wedged it. The manifest now runs `electron-builder --linux dir`, which +only produces an unpacked tree, and the flatpak is assembled around that tree. + +## Prerequisites + +flatpak-builder does not run on macOS. Build on Linux; to produce the arm64 +package you need arm64 hardware or `qemu-user-static` binfmt emulation. + +```sh +flatpak install -y flathub org.freedesktop.Platform//24.08 org.freedesktop.Sdk//24.08 \ + org.freedesktop.Sdk.Extension.node24//24.08 org.electronjs.Electron2.BaseApp//24.08 +``` + +## Generating `generated-sources.json` + +The build sandbox has no network, so every npm tarball and the Electron binary +have to be declared as sources up front. That file is generated, not written by +hand, and is not committed — regenerate it whenever `package-lock.json` changes: + +```sh +# Needs network. Run from the repo root. +pipx install 'git+https://github.com/flatpak/flatpak-builder-tools.git#subdirectory=node' +flatpak-node-generator npm package-lock.json -o flatpak/generated-sources.json +``` + +The generator also emits `flatpak-node/electron-builder-arch-args.sh`, which the +manifest sources so electron-builder targets the architecture being built. + +## CI + +`.github/workflows/flatpak.yml` builds this manifest on `ubuntu-24.04-arm` for +every pull request that touches the app or the packaging, and uploads the +resulting `slacky.flatpak` as a run artifact you can install with +`flatpak install --user slacky.flatpak`. A separate job validates the AppStream +and desktop metadata. + +CI generates `generated-sources.json` itself, and rewrites the manifest's `git` +source to a `dir` source so it builds the commit under test rather than the last +published tag. The committed manifest keeps the `git` source, which is what +Flathub requires. + +## Building and running locally + +```sh +flatpak-builder --user --install --force-clean build-dir \ + flatpak/com.andersonlaverde.slacky.yml +flatpak run com.andersonlaverde.slacky +``` + +## Before submitting to Flathub + +- [ ] **Add a real screenshot.** `com.andersonlaverde.slacky.metainfo.xml` points + at `build/screenshots/main-window.png`, which does not exist yet. Flathub + rejects submissions whose screenshot URLs do not resolve. +- [ ] Validate the metadata: + ```sh + appstreamcli validate flatpak/com.andersonlaverde.slacky.metainfo.xml + desktop-file-validate flatpak/com.andersonlaverde.slacky.desktop + flatpak run --command=flatpak-builder-lint org.flatpak.Builder \ + manifest flatpak/com.andersonlaverde.slacky.yml + ``` +- [ ] Confirm huddles work — mic, camera and screen share — since `--device=all` + is the permission most likely to be questioned in review. +- [ ] Bump `tag:` and `commit:` in the manifest to the release being published, + and add a matching `` entry to the metainfo. +- [ ] Submit: open a pull request against + [flathub/flathub](https://github.com/flathub/flathub) on the `new-pr` + branch adding this manifest. Flathub then creates + `flathub/com.andersonlaverde.slacky`, and future releases are published by + updating the manifest in *that* repo, not this one. +- [ ] Slacky bundles no Slack trademark assets beyond the app icon; double-check + the icon before submission, since Flathub review looks at third-party + branding. diff --git a/flatpak/com.andersonlaverde.slacky.desktop b/flatpak/com.andersonlaverde.slacky.desktop new file mode 100644 index 0000000..e625ea8 --- /dev/null +++ b/flatpak/com.andersonlaverde.slacky.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +Type=Application +Name=Slacky +GenericName=Slack Client +Comment=Slack client for Linux arm64 systems +Exec=slacky %U +Icon=com.andersonlaverde.slacky +Terminal=false +Categories=Network;InstantMessaging; +Keywords=slack;chat;messaging;huddle;workspace; +StartupWMClass=Slacky +MimeType=x-scheme-handler/slack; diff --git a/flatpak/com.andersonlaverde.slacky.metainfo.xml b/flatpak/com.andersonlaverde.slacky.metainfo.xml new file mode 100644 index 0000000..8533a83 --- /dev/null +++ b/flatpak/com.andersonlaverde.slacky.metainfo.xml @@ -0,0 +1,72 @@ + + + com.andersonlaverde.slacky + + Slacky + Slack client for Linux arm64 systems + + CC0-1.0 + MIT + + + Anderson Laverde + + + +

+ Slacky is a desktop Slack client for Linux machines running on arm64 + hardware, where the official Slack desktop app is not available. It wraps + the Slack web client in a native window so it behaves like a normal + desktop application. +

+

Features:

+
    +
  • Sign in to several Slack workspaces and switch between them
  • +
  • Desktop notifications that focus the window when clicked
  • +
  • Huddles, including pop-out huddle windows
  • +
  • Google single sign-on completed inside the app
  • +
  • slack:// deep links open in Slacky rather than a browser
  • +
+
+ + com.andersonlaverde.slacky.desktop + + + + + https://raw.githubusercontent.com/andirsun/Slacky/main/build/screenshots/main-window.png + The Slack workspace running in Slacky + + + + https://github.com/andirsun/Slacky + https://github.com/andirsun/Slacky/issues + https://github.com/andirsun/Slacky + + + + intense + intense + + + + + +

+ First stable release: multiple workspaces, native huddle pop-out + windows, in-app Google SSO, window focus on notification click, and + arm64 AppImage, deb and rpm packages. +

+
+ https://github.com/andirsun/Slacky/releases/tag/v1.0.0 +
+
+
diff --git a/flatpak/com.andersonlaverde.slacky.yml b/flatpak/com.andersonlaverde.slacky.yml new file mode 100644 index 0000000..12ca5b9 --- /dev/null +++ b/flatpak/com.andersonlaverde.slacky.yml @@ -0,0 +1,90 @@ +app-id: com.andersonlaverde.slacky +runtime: org.freedesktop.Platform +runtime-version: '24.08' +sdk: org.freedesktop.Sdk +base: org.electronjs.Electron2.BaseApp +base-version: '24.08' +command: slacky +separate-locales: false + +sdk-extensions: + - org.freedesktop.Sdk.Extension.node24 + +finish-args: + # Electron's zygote/renderer processes talk to each other over the X11 and + # IPC sockets; without --share=ipc it falls back to slow shared memory. + - --share=ipc + - --socket=x11 + - --socket=wayland + - --share=network + # Huddles: microphone and speakers via PulseAudio, GPU acceleration and the + # webcam via --device=all. Screen sharing goes through the portal, which is + # available to every sandbox by default. + # + # --device=all is broader than --device=dri and a Flathub reviewer may ask + # about it. It is here because Chromium reaches /dev/video* directly for + # getUserMedia rather than going through the camera portal; narrow this to + # --device=dri if huddle video ever works without it. + - --socket=pulseaudio + - --device=all + # Desktop notifications and the tray icon. + - --talk-name=org.freedesktop.Notifications + - --talk-name=org.kde.StatusNotifierWatcher + # File uploads and downloads land in the usual places. + - --filesystem=xdg-download + - --filesystem=xdg-documents + - --filesystem=xdg-pictures:ro + +modules: + - name: slacky + buildsystem: simple + build-options: + append-path: /usr/lib/sdk/node24/bin + # Electron's binaries are prebuilt and already stripped; extracting + # debuginfo from them yields nothing and costs minutes. + no-debuginfo: true + env: + # The build sandbox has no network. Both caches are pre-populated by + # the sources in generated-sources.json; electron-builder finds the + # Electron binaries it needs under XDG_CACHE_HOME. + # (The directory format is /run/build/MODULE_NAME/flatpak-node/...) + XDG_CACHE_HOME: /run/build/slacky/flatpak-node/cache + npm_config_cache: /run/build/slacky/flatpak-node/npm-cache + build-commands: + # `npm ci`, not `npm install`: install re-resolves the tree and reached + # for a chokidar that is not in the lockfile, so not in the offline + # sources either. ci installs the lockfile exactly. + - npm ci --offline + - npm run build + # flatpak-node-generator drops the Electron zip flat into the cache, but + # @electron/get looks for it under a sha256-of-the-URL subdirectory and + # so goes to github.com instead, which the sandbox cannot reach. Unpack + # it ourselves and hand electron-builder the result via electronDist. + - mkdir -p electron-dist && unzip -q "$XDG_CACHE_HOME"/electron/electron-v*-linux-arm64.zip -d electron-dist + # `--linux dir` produces an unpacked tree and nothing else. Do NOT use + # electron-builder's own `flatpak` target here: it shells out to + # flatpak-builder, and running flatpak-builder inside a flatpak build is + # what wedged the original attempt at this manifest. + # + # The arch args generated by flatpak-node-generator have to be sourced in + # the same command that uses them: each build-command gets its own shell. + - . flatpak-node/electron-builder-arch-args.sh; ./node_modules/.bin/electron-builder --linux dir --config.electronDist=electron-dist $ELECTRON_BUILDER_ARCH_ARGS + - mkdir -p /app/main + - cp -a dist/packages/linux*unpacked/. /app/main/ + # electron-builder renames the Electron binary to executableName. If that + # ever drifts, the wrapper would exec a missing path and the app would + # fail only at launch, so fail here instead. + - test -x /app/main/slacky + # zypak lets Electron's sandbox work inside flatpak's own sandbox. + - install -Dm755 flatpak/slacky.sh /app/bin/slacky + - install -Dm644 build/icons/icon.png /app/share/icons/hicolor/512x512/apps/com.andersonlaverde.slacky.png + - install -Dm644 flatpak/com.andersonlaverde.slacky.desktop /app/share/applications/com.andersonlaverde.slacky.desktop + - install -Dm644 flatpak/com.andersonlaverde.slacky.metainfo.xml /app/share/metainfo/com.andersonlaverde.slacky.metainfo.xml + sources: + - type: git + url: https://github.com/andirsun/Slacky.git + tag: v1.0.0 + # Keep this commit in sync with the tag above; Flathub requires both. + commit: c0dbd8fe2dd6602574f501454e518f17d1236a92 + # Generated by flatpak-node-generator, see flatpak/README.md. + - generated-sources.json diff --git a/flatpak/slacky.sh b/flatpak/slacky.sh new file mode 100755 index 0000000..2890f5a --- /dev/null +++ b/flatpak/slacky.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# Electron ships its own sandbox, which cannot nest inside flatpak's. zypak +# redirects it onto the flatpak sandbox instead of disabling it. +exec zypak-wrapper /app/main/slacky "$@"