diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01cfcfc..e733f46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable with: components: clippy @@ -23,7 +23,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - run: cargo test diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 0000000..d0dfcba --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,183 @@ +name: Package + +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + branches: [main] + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + +jobs: + rpm: + name: Build RPM + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + + - name: Install cargo-generate-rpm + run: cargo install cargo-generate-rpm --locked + + - name: Compute version metadata + id: ver + # On tag pushes (refs/tags/v*) emit a clean release (`release = "1"`) + # and validate that the tag matches Cargo.toml; on all other triggers + # emit a dev-build release tag with run number + short SHA. + run: | + set -euo pipefail + version=$(grep -E '^version *= *' Cargo.toml | head -n1 | cut -d'"' -f2) + short_sha="${GITHUB_SHA::7}" + + if [ "${GITHUB_REF_TYPE}" = "tag" ]; then + tag_version="${GITHUB_REF_NAME#v}" + if [ "$tag_version" != "$version" ]; then + echo "::error::Tag ${GITHUB_REF_NAME} does not match Cargo.toml version ${version}" >&2 + exit 1 + fi + rpm_release="1" + artifact_name="presence-switch-rpm-${GITHUB_REF_NAME}" + else + rpm_release="0.dev.${GITHUB_RUN_NUMBER}.git${short_sha}" + artifact_name="presence-switch-rpm-${short_sha}" + fi + + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "short_sha=$short_sha" >> "$GITHUB_OUTPUT" + echo "rpm_release=$rpm_release" >> "$GITHUB_OUTPUT" + echo "artifact_name=$artifact_name" >> "$GITHUB_OUTPUT" + + - name: Compile release binary + run: cargo build --release --locked + + - name: Generate RPM + run: | + mkdir -p target/generate-rpm + cargo generate-rpm \ + --output "target/generate-rpm/presence-switch-${{ steps.ver.outputs.version }}-${{ steps.ver.outputs.rpm_release }}.x86_64.rpm" \ + -s 'release = "${{ steps.ver.outputs.rpm_release }}"' + + - name: Upload RPM artifact + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.ver.outputs.artifact_name }} + path: target/generate-rpm/*.rpm + if-no-files-found: error + + msi: + name: Build MSI (Linux cross-compile) + runs-on: ubuntu-latest + + env: + WIN_TARGET: x86_64-pc-windows-gnu + + steps: + - uses: actions/checkout@v6 + + - name: Install mingw-w64 and wixl + # Debian/Ubuntu split wixl out of msitools — install both. + run: | + sudo apt-get update + sudo apt-get install -y gcc-mingw-w64-x86-64 msitools wixl + + - uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-pc-windows-gnu + + - uses: Swatinem/rust-cache@v2 + with: + key: windows-gnu + + - name: Compute version metadata + id: ver + run: | + set -euo pipefail + version=$(grep -E '^version *= *' Cargo.toml | head -n1 | cut -d'"' -f2) + short_sha="${GITHUB_SHA::7}" + + if [ "${GITHUB_REF_TYPE}" = "tag" ]; then + tag_version="${GITHUB_REF_NAME#v}" + if [ "$tag_version" != "$version" ]; then + echo "::error::Tag ${GITHUB_REF_NAME} does not match Cargo.toml version ${version}" >&2 + exit 1 + fi + msi_filename="presence-switch-${version}.msi" + artifact_name="presence-switch-msi-${GITHUB_REF_NAME}" + else + msi_filename="presence-switch-${version}-${short_sha}.msi" + artifact_name="presence-switch-msi-${short_sha}" + fi + + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "short_sha=$short_sha" >> "$GITHUB_OUTPUT" + echo "msi_filename=$msi_filename" >> "$GITHUB_OUTPUT" + echo "artifact_name=$artifact_name" >> "$GITHUB_OUTPUT" + + - name: Cross-compile Windows binary + env: + # Rust auto-detects the mingw linker by triple, but be explicit so + # this doesn't silently regress if the apt package layout changes. + CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: x86_64-w64-mingw32-gcc + run: cargo build --release --locked --target "${WIN_TARGET}" + + - name: Build MSI with wixl + run: | + mkdir -p target/wix + wixl \ + --arch x64 \ + -D "Version=${{ steps.ver.outputs.version }}" \ + -D "ExePath=target/${WIN_TARGET}/release/presence-switch.exe" \ + --output "target/wix/${{ steps.ver.outputs.msi_filename }}" \ + wix/main.wxs + + - name: Upload MSI artifact + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.ver.outputs.artifact_name }} + path: target/wix/*.msi + if-no-files-found: error + + release: + name: Publish GitHub Release + needs: [rpm, msi] + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + + permissions: + contents: write + + steps: + - uses: actions/checkout@v6 + + - name: Download RPM artifact + uses: actions/download-artifact@v8 + with: + name: presence-switch-rpm-${{ github.ref_name }} + path: artifacts/ + + - name: Download MSI artifact + uses: actions/download-artifact@v8 + with: + name: presence-switch-msi-${{ github.ref_name }} + path: artifacts/ + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + ls -la artifacts/ + # --generate-notes auto-generates the body from commits since last + # release; --verify-tag fails fast if the tag isn't actually pushed. + gh release create "${GITHUB_REF_NAME}" \ + --title "${GITHUB_REF_NAME}" \ + --generate-notes \ + --verify-tag \ + artifacts/*.rpm artifacts/*.msi diff --git a/.gitignore b/.gitignore index 4fd92fe..e335923 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target +/vendor .claude/settings.local.json diff --git a/Cargo.toml b/Cargo.toml index 8fe246d..4659c51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,9 @@ name = "presence-switch" version = "0.1.0" edition = "2024" +license = "MIT" +description = "Discord Rich Presence IPC proxy that multiplexes RPC messages across multiple Discord instances" +repository = "https://github.com/kramerc/presence-switch" [dependencies] lazy_static = "1.5.0" @@ -13,3 +16,16 @@ tokio-util = "0.7.18" tracing = "0.1.44" tracing-subscriber = "0.3.23" windows-sys = "0.61.2" + +# RPM packaging via cargo-generate-rpm. +# `name`, `version`, `license`, and `summary` are inherited from [package]. +# Override `release` at build time for dev builds: +# cargo generate-rpm -s 'release = "0.dev..git"' +[package.metadata.generate-rpm] +release = "1" +assets = [ + { source = "target/release/presence-switch", dest = "/usr/bin/presence-switch", mode = "755" }, + { source = "packaging/linux/systemd/presence-switch.service", dest = "/usr/lib/systemd/user/presence-switch.service", mode = "644" }, + { source = "LICENSE", dest = "/usr/share/licenses/presence-switch/LICENSE", mode = "644", doc = true }, + { source = "README.md", dest = "/usr/share/doc/presence-switch/README.md", mode = "644", doc = true }, +] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..19147b8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Kramer Campbell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 500f196..d4ee20d 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,35 @@ The IPC binary protocol uses a simple format: 4-byte LE opcode + 4-byte LE lengt cargo build --release ``` +## Installing + +Tagged releases publish `.rpm` and `.msi` builds to the [Releases](https://github.com/kramerc/presence-switch/releases) page. For unreleased changes, the [`Package`](.github/workflows/package.yml) workflow also produces dev artifacts on every push to `main` and every PR — download them from the workflow run's Artifacts section. + +To build packages locally: + +```sh +scripts/package.sh rpm # → target/generate-rpm/presence-switch-*.rpm +scripts/package.sh msi # → target/wix/presence-switch-*.msi (cross-compiled from Linux) +scripts/package.sh all +``` + +See `scripts/package.sh --help` for the toolchain requirements. + +### Linux (any RPM-based distro with systemd) + +```sh +sudo dnf install ./presence-switch-*.rpm # Fedora, RHEL, CentOS, Rocky, Alma +sudo zypper install ./presence-switch-*.rpm # openSUSE +systemctl --user daemon-reload +systemctl --user enable --now presence-switch +``` + +The package installs a per-user systemd unit at `/usr/lib/systemd/user/presence-switch.service`. View logs with `journalctl --user -u presence-switch`. CI builds the RPM against Ubuntu 24.04's glibc (2.39+), so the target distro needs glibc ≥ 2.39 — that covers Fedora 41+, RHEL 10+, recent openSUSE Tumbleweed, and similar. + +### Windows + +Double-click the `.msi` to install per-user (no admin prompt). The installer adds an entry under `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` so presence-switch launches at every logon — inspect or disable it via *Task Manager → Startup apps*. Uninstall via *Settings → Apps & features*. + ## Usage 1. Close Discord or ensure `discord-ipc-0` is not taken @@ -76,3 +105,22 @@ src/ ├── unix.rs # Unix domain socket connection └── windows.rs # Named pipe connection ``` + +## Releasing + +To cut a new release: + +1. Bump `version` in `Cargo.toml` (and run `cargo update -w` so `Cargo.lock` matches). +2. Commit the bump and merge to `main`. +3. Tag the commit on `main` matching the new version, e.g.: + ```sh + git tag v0.2.0 + git push origin v0.2.0 + ``` +4. The [`Package`](.github/workflows/package.yml) workflow runs on the tag, validates that the tag matches `Cargo.toml`, builds the `.rpm` and `.msi`, and publishes a GitHub Release with both attached and auto-generated notes from the commits since the previous release. + +If the tag version doesn't match `Cargo.toml`'s `version` field, both build jobs fail loudly before doing any work. + +## License + +[MIT](LICENSE) © Kramer Campbell diff --git a/packaging/linux/systemd/presence-switch.service b/packaging/linux/systemd/presence-switch.service new file mode 100644 index 0000000..1d5d8c1 --- /dev/null +++ b/packaging/linux/systemd/presence-switch.service @@ -0,0 +1,15 @@ +[Unit] +Description=Discord Rich Presence IPC proxy +Documentation=https://github.com/kramerc/presence-switch +After=graphical-session.target +PartOf=graphical-session.target + +[Service] +Type=simple +ExecStart=/usr/bin/presence-switch +Restart=on-failure +RestartSec=3 +NoNewPrivileges=true + +[Install] +WantedBy=default.target diff --git a/scripts/package.sh b/scripts/package.sh new file mode 100755 index 0000000..cd3eae4 --- /dev/null +++ b/scripts/package.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# Local packaging script for presence-switch. +# +# Mirrors the steps run by .github/workflows/package.yml so you can reproduce +# CI builds (or just inspect failures) without pushing. +# +# Usage: +# scripts/package.sh rpm Build the .rpm into target/generate-rpm/ +# scripts/package.sh msi Cross-compile the Windows binary and build the .msi +# scripts/package.sh all Both +# +# Required for RPM: cargo, cargo-generate-rpm (cargo install cargo-generate-rpm) +# Required for MSI: mingw64-gcc msitools (provides wixl) +# plus rustup target x86_64-pc-windows-gnu + +set -euo pipefail + +# Ensure tooling installed by rustup is findable, regardless of whether the +# user has ~/.cargo/bin in their shell rc. +export PATH="$HOME/.cargo/bin:$PATH" + +cd "$(dirname "$0")/.." +ROOT=$(pwd) +NAME=presence-switch +VERSION=$(grep -E '^version *= *' Cargo.toml | head -n1 | cut -d'"' -f2) + +if SHORT_SHA=$(git rev-parse --short=7 HEAD 2>/dev/null); then + if ! git diff-index --quiet HEAD -- 2>/dev/null; then + SHORT_SHA="${SHORT_SHA}.dirty" + fi +else + SHORT_SHA="nogit" +fi + +LOCAL_RELEASE="0.local.${SHORT_SHA}" + +log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } +fail() { printf '\033[1;31mERROR:\033[0m %s\n' "$*" >&2; exit 1; } +need() { command -v "$1" >/dev/null 2>&1 || fail "$1 not found. $2"; } + +build_rpm() { + log "Building RPM name=${NAME} version=${VERSION} release=${LOCAL_RELEASE}" + + need cargo "Install rustup: https://rustup.rs" + need cargo-generate-rpm "Install: cargo install cargo-generate-rpm --locked" + + log "Compiling release binary" + cargo build --release --locked + + log "Generating RPM" + mkdir -p target/generate-rpm + cargo generate-rpm \ + --output "target/generate-rpm/${NAME}-${VERSION}-${LOCAL_RELEASE}.x86_64.rpm" \ + -s "release = \"${LOCAL_RELEASE}\"" + + local built="${ROOT}/target/generate-rpm/${NAME}-${VERSION}-${LOCAL_RELEASE}.x86_64.rpm" + log "Built RPM: ${built}" +} + +build_msi() { + log "Building MSI name=${NAME} version=${VERSION} sha=${SHORT_SHA}" + + need cargo "Install rustup: https://rustup.rs" + need x86_64-w64-mingw32-gcc "Install: sudo dnf install mingw64-gcc" + need wixl "Install: sudo dnf install msitools" + + if ! rustup target list --installed 2>/dev/null | grep -q '^x86_64-pc-windows-gnu$'; then + log "Adding rustup target x86_64-pc-windows-gnu" + rustup target add x86_64-pc-windows-gnu + fi + + log "Cross-compiling presence-switch.exe" + CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER=x86_64-w64-mingw32-gcc \ + cargo build --release --locked --target x86_64-pc-windows-gnu + + log "Linking MSI" + mkdir -p target/wix + local out="target/wix/${NAME}-${VERSION}-${SHORT_SHA}.msi" + # -D Version threads the Cargo.toml version into the MSI's ProductVersion + # so MajorUpgrade detection stays correct as the project version bumps. + wixl \ + --arch x64 \ + -D "Version=${VERSION}" \ + -D "ExePath=target/x86_64-pc-windows-gnu/release/${NAME}.exe" \ + --output "${out}" \ + wix/main.wxs + log "Built MSI: ${ROOT}/${out}" +} + +usage() { + cat < + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +