Skip to content
Open
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
7 changes: 7 additions & 0 deletions .claude/rules/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ Not part of `ci.yml`, not in the default run:
- **devenv CI** (path-filtered): format `devenv.nix` and `devenv --no-tui shell -- true`.
- **Build**: unsigned installers on every PR. Run the matching `cargo xtask`
package command only when the diff touches packaging.
- **Build / Linux Flatpak**: gated behind the `needs: build` label like the rest
of that matrix, so a PR touching `packaging/linux/flatpak/**` gets no feedback
until someone labels it. Reproduce locally with
`flatpak-builder --user --disable-rofiles-fuse --force-clean --repo=repo
--default-branch=stable build packaging/linux/flatpak/org.openlogi.OpenLogi.yml`,
and keep the build tree off `/tmp` if it is a tmpfs: the workspace needs well
over 10 GB to compile inside the sandbox.

## When you add a CI job

Expand Down
127 changes: 126 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ on:
linux_result:
description: Result of the Linux packages build job.
value: ${{ jobs.results.outputs.linux }}
linux_flatpak_result:
description: Result of the Linux Flatpak bundle build job.
value: ${{ jobs.results.outputs.linux_flatpak }}
# PR builds are opt-in: add the `needs: build` label to produce unsigned
# installers for testing. `closed` / unlabel cancel an in-flight run via
# concurrency (jobs below no-op on those actions).
Expand Down Expand Up @@ -875,6 +878,126 @@ jobs:
name: OpenLogi-linux-packages-${{ matrix.arch }}
path: dist/*

# The same tree the .deb/.rpm are cut from, packaged as a single-file Flatpak
# bundle for the distributions where neither is native. Separate from
# linux-packages rather than folded into it: this build compiles the whole
# workspace again inside the Flatpak SDK, where the runner's rust-cache cannot
# reach, so pairing them would tie a thirty-minute job to an hour-long one and
# lose the .deb when the Flatpak leg failed.
linux-flatpak:
name: Linux Flatpak (${{ matrix.arch }})
needs: should-build
if: ${{ needs.should-build.outputs.run == 'true' }}
runs-on: ${{ matrix.runner }}
# Measured at ~10 minutes cold on ubuntu-latest, so this is a bound on a
# hang rather than an estimate — sized like linux-packages' 30 above, with
# room for the arm runner being slower.
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm
permissions:
contents: read
steps:
- uses: actions/checkout@v6

# Native builds, like the packages above: the bundle is labelled with this
# arch, so a mismatched runner would ship a wrongly-named file.
- name: Verify runner architecture
env:
EXPECTED_ARCH: ${{ matrix.arch }}
run: |
case "$(uname -m)" in
x86_64) got=amd64 ;;
aarch64) got=arm64 ;;
*) echo "unexpected machine $(uname -m)" >&2; exit 1 ;;
esac
test "$got" = "$EXPECTED_ARCH"

- name: Install flatpak and the runtime
env:
RUNTIME_VERSION: "25.08"
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y flatpak flatpak-builder
flatpak remote-add --if-not-exists --user \
flathub https://dl.flathub.org/repo/flathub.flatpakrepo
# llvm20 because openlogi-camera pulls v4l2-sys-mit, whose build
# script runs bindgen and dlopens libclang; the base SDK ships none.
flatpak install --user -y --noninteractive flathub \
"org.freedesktop.Platform//${RUNTIME_VERSION}" \
"org.freedesktop.Sdk//${RUNTIME_VERSION}" \
"org.freedesktop.Sdk.Extension.llvm20//${RUNTIME_VERSION}"

# AppStream is what software centres read, and a stale <release> would
# advertise a version the bundle is not. Both values come from the tree
# being built rather than from an input, so the label cannot drift from
# the binaries the way a version passed by hand can.
- name: Stamp the AppStream release
run: |
set -euo pipefail
version="$(cargo metadata --no-deps --format-version 1 \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["packages"][0]["version"])')"
# The commit's own date, not today's: a rebuild of an old tag must
# not claim to have been released the day it was rebuilt.
released="$(git log -1 --format=%cs)"
echo "stamping ${version} (${released})"
python3 - "$version" "$released" <<'PY'
import re, sys
version, released = sys.argv[1], sys.argv[2]
path = "packaging/linux/flatpak/org.openlogi.OpenLogi.metainfo.xml"
text = open(path, encoding="utf-8").read()
text, n = re.subn(
r'<release version="[^"]*" date="[^"]*"/>',
f'<release version="{version}" date="{released}"/>',
text,
count=1,
)
if n != 1:
raise SystemExit("no <release> element to stamp in " + path)
open(path, "w", encoding="utf-8").write(text)
PY
grep '<release ' packaging/linux/flatpak/org.openlogi.OpenLogi.metainfo.xml

- name: Build the bundle
env:
PKG_ARCH: ${{ matrix.arch }}
run: |
set -euo pipefail
# --disable-rofiles-fuse: the copy-on-write mount flatpak-builder
# prefers needs a FUSE mount the runner will not always grant, and
# failing that way looks like a build error rather than an
# environment one. The build is a throwaway tree here, so the
# protection it provides buys nothing.
flatpak-builder --user --disable-rofiles-fuse --force-clean \
--repo=repo --default-branch=stable \
build packaging/linux/flatpak/org.openlogi.OpenLogi.yml

mkdir -p dist
ref_name="${GITHUB_REF_NAME:-dev}"
ref_name="${ref_name//\//-}"
# --runtime-repo so installing the bundle knows where to get the
# runtime it needs; without it the install fails on a machine that
# does not already have the freedesktop platform.
flatpak build-bundle \
--runtime-repo=https://dl.flathub.org/repo/flathub.flatpakrepo \
repo "dist/openlogi-${ref_name}-linux-${PKG_ARCH}.flatpak" \
org.openlogi.OpenLogi stable
ls -lh dist/

- uses: actions/upload-artifact@v7
with:
name: OpenLogi-linux-flatpak-${{ matrix.arch }}
path: dist/*
# Already a compressed OSTree stream; zipping it again buys nothing.
compression-level: 0

# Single owner of the GitHub Release. softprops/action-gh-release@v3 creates the
# release as a draft, uploads every asset to that (mutable) draft, then publishes
# it as its final step — so the release only becomes immutable once all DMGs and
Expand All @@ -889,17 +1012,19 @@ jobs:
results:
name: Build results
if: ${{ always() }}
needs: [should-build, macos, windows, windows-msi, linux-packages]
needs: [should-build, macos, windows, windows-msi, linux-packages, linux-flatpak]
runs-on: ubuntu-latest
outputs:
macos: ${{ needs.macos.result }}
windows: ${{ needs.windows.result }}
windows_msi: ${{ needs['windows-msi'].result }}
linux: ${{ needs['linux-packages'].result }}
linux_flatpak: ${{ needs['linux-flatpak'].result }}
steps:
- name: Summarize
run: |
echo "macos=${{ needs.macos.result }}"
echo "windows=${{ needs.windows.result }}"
echo "windows-msi=${{ needs['windows-msi'].result }}"
echo "linux-packages=${{ needs['linux-packages'].result }}"
echo "linux-flatpak=${{ needs['linux-flatpak'].result }}"
27 changes: 20 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,15 @@ jobs:
path: dist
merge-multiple: true

# Best-effort alongside the packages, and separate from them: the Flatpak
# leg builds the whole workspace again inside the SDK, so it is the
# slowest Linux job and the one most likely to be the single leg missing.
- uses: actions/download-artifact@v8
with:
pattern: OpenLogi-linux-flatpak-*
path: dist
merge-multiple: true

- name: Generate checksums
run: |
cd dist
Expand All @@ -176,7 +185,7 @@ jobs:
# hashed alone. The Windows portable artifact is a zip of the GUI +
# agent exes (#347); no bare .exe ships anymore.
shopt -s nullglob
sha256sum -- *.dmg *.zip *.msi *.deb *.rpm *.pkg.tar.zst > SHA256SUMS
sha256sum -- *.dmg *.zip *.msi *.deb *.rpm *.pkg.tar.zst *.flatpak > SHA256SUMS

# softprops' `files` can't list a glob that matches nothing without
# tripping fail_on_unmatched_files. The Windows zip/msi (per arch leg)
Expand All @@ -187,7 +196,7 @@ jobs:
- name: Detect best-effort artifacts
id: artifacts
run: |
for ext in zip msi deb rpm; do
for ext in zip msi deb rpm flatpak; do
if compgen -G "dist/*.$ext" > /dev/null; then
echo "$ext=true" >> "$GITHUB_OUTPUT"
else
Expand Down Expand Up @@ -256,12 +265,15 @@ jobs:
# The Windows binaries and Linux packages get the same minisign
# treatment as the DMGs: manual verification today, and the future
# auto-updaters need the detached signatures to exist for every
# shipped version.
# nullglob: the zip/msi/deb/rpm sets are best-effort per arch leg, so
# the globs may match nothing; the DMGs are guaranteed by the publish
# gate.
# shipped version. The Flatpak bundle is in that set for the same
# reason — it is attached to the release and copied to R2 like the
# rest, so leaving it unsigned would make it the one installer a user
# cannot verify.
# nullglob: the zip/msi/deb/rpm/flatpak sets are best-effort per arch
# leg, so the globs may match nothing; the DMGs are guaranteed by the
# publish gate.
shopt -s nullglob
for artifact in dist/*.dmg dist/*.zip dist/*.msi dist/*.deb dist/*.rpm dist/*.pkg.tar.zst; do
for artifact in dist/*.dmg dist/*.zip dist/*.msi dist/*.deb dist/*.rpm dist/*.pkg.tar.zst dist/*.flatpak; do
minisign -S -m "$artifact" -s "$key_file" -x "$artifact.minisig" -W
minisign -V -m "$artifact" -P "$OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY" -x "$artifact.minisig"
done
Expand Down Expand Up @@ -330,6 +342,7 @@ jobs:
${{ steps.artifacts.outputs.deb == 'true' && 'dist/*.deb' || '' }}
${{ steps.artifacts.outputs.rpm == 'true' && 'dist/*.rpm' || '' }}
${{ steps.artifacts.outputs.pacman == 'true' && 'dist/*.pkg.tar.zst' || '' }}
${{ steps.artifacts.outputs.flatpak == 'true' && 'dist/*.flatpak' || '' }}
fail_on_unmatched_files: true

homebrew-tap:
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,8 @@ node_modules/

# Packaging output.
*.dmg

# flatpak-builder output and scratch state.
/.flatpak-builder/
/build/
/repo/
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,27 @@ sudo pacman -U openlogi-*.pkg.tar.zst
Packages are published for both `x86_64`/`amd64` and `arm64`/`aarch64`.
Pre-built packages require GLIBC 2.35 or newer (Ubuntu 22.04 baseline).

Each release also carries a Flatpak bundle, for `amd64` and `arm64`:

```sh
flatpak install --user ./openlogi-*.flatpak
```

It is a one-time install: there is no remote behind it, so `flatpak update` has
nothing to check and a new version means downloading the next bundle.

A Flatpak cannot write to `/etc`, so its udev rules go on the host separately.
They ship inside the application, so this needs no extra download:

```sh
flatpak run --command=cat org.openlogi.OpenLogi /app/share/openlogi/udev/70-openlogi.rules | sudo tee /etc/udev/rules.d/70-openlogi.rules >/dev/null && sudo udevadm control --reload-rules && sudo udevadm trigger
```

Until that runs, no devices are detected at all. Nothing extra is needed for
`/dev/uinput`: the rules create its node at boot with `static_node=uinput`, and
opening it loads the module. The agent is launched by the application itself, so
unlike the packages below there is no user service to enable.

NixOS users can instead import the repository's module, which installs the
package and udev rules and starts the agent with the graphical session:

Expand Down
46 changes: 46 additions & 0 deletions docs/INSTALL-linux.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,52 @@ For a build without installing the module:
nix build github:AprilNEA/OpenLogi#openlogi
```

## Flatpak

Each release carries a `.flatpak` bundle for `amd64` and `arm64`, on the
[releases page](https://github.com/AprilNEA/OpenLogi/releases/latest):

```sh
flatpak install --user ./openlogi-*.flatpak
```

It is a one-time install. There is no remote behind a bundle, so `flatpak
update` has nothing to check and a new version means downloading the next one.

> [!IMPORTANT]
> A Flatpak cannot write to `/etc`, so the udev rules have to be installed on
> the host separately. **Until you do, no devices are detected at all**: the
> sandbox is allowed to reach `/dev/hidraw*`, `/dev/uinput` and
> `/dev/input/event*`, but the kernel still refuses your user access to them,
> so the app opens to an empty device list.

The same rules the packages install ship inside the app, so this needs no
checkout and no extra download:

```sh
flatpak run --command=cat org.openlogi.OpenLogi \
/app/share/openlogi/udev/70-openlogi.rules |
sudo tee /etc/udev/rules.d/70-openlogi.rules >/dev/null

sudo udevadm control --reload-rules
sudo udevadm trigger
```

Nothing extra is needed for `/dev/uinput`: the rules create its node at boot
with `static_node=uinput`, and opening it loads the module. See
[Device access: udev rules](#device-access-udev-rules) for what the rules grant
and how to verify them.

The agent installs its input hook once at startup, so restart the app after
granting access:

```sh
flatpak kill org.openlogi.OpenLogi
```

Then launch it again. There is no user service to enable — unlike the packages,
the application launches its own agent.

## Build from source

Pre-built `.deb` and `.rpm` packages are available on the
Expand Down
15 changes: 15 additions & 0 deletions packaging/linux/flatpak/org.openlogi.OpenLogi.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[Desktop Entry]
Type=Application
Name=OpenLogi
Comment=Logitech HID++ device control: remap buttons, DPI, SmartShift
Exec=openlogi-desktop
Icon=org.openlogi.OpenLogi
Terminal=false
# Ties the running window (Wayland xdg-toplevel app_id / X11 WM_CLASS) back to
# this launcher. The GUI advertises `openlogi_core::brand::APP_ID`, which is
# lowercase and therefore differs from the flatpak application ID; without
# this key the shell shows a generic icon for the window.
StartupWMClass=org.openlogi.openlogi
Categories=Settings;HardwareSettings;
Keywords=logitech;mouse;hid;remap;dpi;
StartupNotify=true
Loading
Loading