From c2de9258f5b8ffa3b3ac0bafc54d8c89771bef14 Mon Sep 17 00:00:00 2001 From: Ryan Draga Date: Wed, 14 Oct 2026 23:47:20 -0400 Subject: [PATCH] MINOR: Add extra Linux packaging options --- .github/scripts/package-linux.sh | 241 ++++++++++++++++++ .github/workflows/ci-release.yml | 14 +- Makefile | 12 +- README.md | 29 ++- app/src/ui/icon.rs | 5 + .../linux/com.boxingoctopus.grimoire.desktop | 11 + .../com.boxingoctopus.grimoire.metainfo.xml | 17 ++ packaging/linux/grimoire.spec.in | 45 ++++ 8 files changed, 357 insertions(+), 17 deletions(-) create mode 100755 .github/scripts/package-linux.sh create mode 100644 packaging/linux/com.boxingoctopus.grimoire.desktop create mode 100644 packaging/linux/com.boxingoctopus.grimoire.metainfo.xml create mode 100644 packaging/linux/grimoire.spec.in diff --git a/.github/scripts/package-linux.sh b/.github/scripts/package-linux.sh new file mode 100755 index 0000000..b3e8a4a --- /dev/null +++ b/.github/scripts/package-linux.sh @@ -0,0 +1,241 @@ +#!/usr/bin/env bash +# Build a Grimoire tarball, .deb, and .rpm from a release binary. +# Usage: package-linux.sh +# +# arch-label is linux_x64 or linux_arm64, matching the other release assets. +# The packages install /usr/bin/grimoire, a desktop entry, and hicolor icons. +# Linked libraries become package dependencies. Wayland and Vulkan are dlopen'd, +# so those dependencies are added explicitly. +set -euo pipefail + +BIN="${1:?path to the Grimoire binary}" +OUT="${2:?output directory}" +ARCH="${3:?arch label, such as linux_x64}" + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +APP_ID="com.boxingoctopus.grimoire" +MAINTAINER="Boxing Octopus Creative Engineering " +HOMEPAGE="https://grimoirebooks.app" + +VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' "$ROOT/app/Cargo.toml" | head -1)" +if [[ -z "${VERSION}" ]]; then + echo "Could not read version from app/Cargo.toml" >&2 + exit 1 +fi +if [[ ! -f "${BIN}" ]]; then + echo "Missing binary: ${BIN}" >&2 + exit 1 +fi + +case "${ARCH}" in + linux_x64) + DEB_ARCH="amd64" + RPM_ARCH="x86_64" + MACHINE_PAT="X86-64" + ;; + linux_arm64) + DEB_ARCH="arm64" + RPM_ARCH="aarch64" + MACHINE_PAT="AArch64" + ;; + *) + echo "Unknown arch label: ${ARCH} (expected linux_x64 or linux_arm64)" >&2 + exit 1 + ;; +esac + +missing=() +for cmd in readelf objdump dpkg-deb dpkg-shlibdeps rpmbuild; do + if ! command -v "${cmd}" >/dev/null 2>&1; then + missing+=("${cmd}") + fi +done +if [[ ${#missing[@]} -gt 0 ]]; then + echo "Missing packaging tools: ${missing[*]}" >&2 + echo "On Ubuntu: sudo apt-get install binutils dpkg-dev rpm desktop-file-utils" >&2 + exit 1 +fi + +HEADER="$(readelf -h "${BIN}")" +if ! grep -q 'ELF64' <<<"${HEADER}"; then + echo "Not a 64-bit ELF binary: ${BIN}" >&2 + exit 1 +fi +if ! grep -q "${MACHINE_PAT}" <<<"${HEADER}"; then + echo "Binary machine does not match ${ARCH}:" >&2 + echo "${HEADER}" >&2 + exit 1 +fi + +DATE="$(date -u +%F)" +STAGE="$(mktemp -d "${TMPDIR:-/tmp}/grimoire-linux.XXXXXX")" +PAYLOAD="${STAGE}/root" +DEB_ROOT="${STAGE}/deb" + +cleanup() { + rm -rf "${STAGE}" +} +trap cleanup EXIT + +install_icon() { + local size="$1" + local src="$2" + install -D -m 0644 "${src}" \ + "${PAYLOAD}/usr/share/icons/hicolor/${size}/apps/${APP_ID}.png" +} + +install -D -m 0755 "${BIN}" "${PAYLOAD}/usr/bin/grimoire" +install -D -m 0644 "${ROOT}/packaging/linux/${APP_ID}.desktop" \ + "${PAYLOAD}/usr/share/applications/${APP_ID}.desktop" +sed \ + -e "s/@VERSION@/${VERSION}/g" \ + -e "s/@DATE@/${DATE}/g" \ + "${ROOT}/packaging/linux/${APP_ID}.metainfo.xml" \ + > "${STAGE}/metainfo.xml" +install -D -m 0644 "${STAGE}/metainfo.xml" \ + "${PAYLOAD}/usr/share/metainfo/${APP_ID}.metainfo.xml" +install -D -m 0644 "${ROOT}/LICENSE" \ + "${PAYLOAD}/usr/share/licenses/grimoire/MPL-2.0" +install -D -m 0644 "${ROOT}/LICENSE" \ + "${PAYLOAD}/usr/share/doc/grimoire/copyright" + +install_icon 32x32 "${ROOT}/app/icons/32x32.png" +install_icon 64x64 "${ROOT}/app/icons/64x64.png" +install_icon 128x128 "${ROOT}/app/icons/128x128.png" +install_icon 256x256 "${ROOT}/app/icons/128x128@2x.png" +install_icon 512x512 "${ROOT}/app/icons/icon.png" + +if command -v desktop-file-validate >/dev/null 2>&1; then + desktop-file-validate "${PAYLOAD}/usr/share/applications/${APP_ID}.desktop" +fi + +mkdir -p "${OUT}" +TAR="${OUT}/Grimoire_${VERSION}_${ARCH}.tar.gz" +rm -f "${TAR}" +tar -C "$(dirname "${BIN}")" -czf "${TAR}" "$(basename "${BIN}")" + +# Versioned Depends from the binary's DT_NEEDED entries, plus libraries the +# process loads with dlopen (they never show up in NEEDED). +# debian/ is only for dpkg-shlibdeps. dpkg-deb packages DEBIAN/, not debian/. +mkdir -p "${DEB_ROOT}/debian" +cp -a "${PAYLOAD}/." "${DEB_ROOT}/" +cat > "${DEB_ROOT}/debian/control" <"${SHLIB_ERR}" +)" || { + cat "${SHLIB_ERR}" >&2 + exit 1 +} +# Expected when the binary is not staged under debian//. +if [[ -s "${SHLIB_ERR}" ]]; then + grep -v "binaries to analyze should already be installed in their package's directory" \ + "${SHLIB_ERR}" >&2 || true +fi +DEPENDS="$(sed -n 's/^shlibs:Depends=//p' <<<"${SHLIBS}")" +if [[ -z "${DEPENDS}" ]]; then + echo "dpkg-shlibdeps did not report dependencies:" >&2 + echo "${SHLIBS}" >&2 + exit 1 +fi + +add_dep() { + local pkg="$1" + case ", ${DEPENDS}," in + *", ${pkg},"* | *", ${pkg} ("*) return ;; + esac + DEPENDS="${DEPENDS}, ${pkg}" +} + +add_dep "libwayland-client0" +add_dep "libvulkan1" +add_dep "hicolor-icon-theme" + +if command -v apt-cache >/dev/null 2>&1; then + while IFS= read -r pkg; do + pkg="${pkg%% (*}" + pkg="$(echo "${pkg}" | tr -d ' ')" + [[ -z "${pkg}" ]] && continue + if ! apt-cache show "${pkg}" >/dev/null 2>&1; then + echo "Debian package not found: ${pkg}" >&2 + exit 1 + fi + done < <(echo "${DEPENDS}" | tr ',' '\n') +fi + +rm -rf "${DEB_ROOT}/debian" +INSTALLED_SIZE="$(du -sk "${PAYLOAD}" | awk '{print $1}')" +mkdir -p "${DEB_ROOT}/DEBIAN" +cat > "${DEB_ROOT}/DEBIAN/control" < "${DEB_ROOT}/DEBIAN/postinst" <<'EOF' +#!/bin/sh +set -e +if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database -q /usr/share/applications || true +fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + gtk-update-icon-cache -q /usr/share/icons/hicolor || true +fi +EOF +cp "${DEB_ROOT}/DEBIAN/postinst" "${DEB_ROOT}/DEBIAN/postrm" +chmod 0755 "${DEB_ROOT}/DEBIAN/postinst" "${DEB_ROOT}/DEBIAN/postrm" + +DEB="${OUT}/Grimoire_${VERSION}_${ARCH}.deb" +rm -f "${DEB}" +dpkg-deb --root-owner-group --build "${DEB_ROOT}" "${DEB}" >/dev/null + +FILELIST="${STAGE}/files.list" +{ + echo "%dir /usr/share/doc/grimoire" + echo "%dir /usr/share/licenses/grimoire" + find "${PAYLOAD}/usr" -type f | sed "s|^${PAYLOAD}||" | sort +} > "${FILELIST}" + +SPEC="${STAGE}/grimoire.spec" +sed \ + -e "s|@VERSION@|${VERSION}|g" \ + -e "s|@RPM_ARCH@|${RPM_ARCH}|g" \ + -e "s|@PAYLOAD@|${PAYLOAD}|g" \ + -e "s|@FILELIST@|${FILELIST}|g" \ + "${ROOT}/packaging/linux/grimoire.spec.in" > "${SPEC}" + +RPM_TOP="${STAGE}/rpm" +mkdir -p "${RPM_TOP}/BUILD" "${RPM_TOP}/RPMS" "${RPM_TOP}/SOURCES" "${RPM_TOP}/SPECS" "${RPM_TOP}/SRPMS" +rpmbuild --define "_topdir ${RPM_TOP}" -bb "${SPEC}" +mapfile -t BUILT_RPMS < <(find "${RPM_TOP}/RPMS" -type f -name '*.rpm' | sort) +if [[ ${#BUILT_RPMS[@]} -ne 1 ]]; then + echo "rpmbuild produced ${#BUILT_RPMS[@]} rpm files, expected 1" >&2 + printf ' %s\n' "${BUILT_RPMS[@]}" >&2 + exit 1 +fi +RPM="${OUT}/Grimoire_${VERSION}_${ARCH}.rpm" +rm -f "${RPM}" +cp "${BUILT_RPMS[0]}" "${RPM}" + +echo "Wrote ${TAR}" +echo "Wrote ${DEB}" +echo "Wrote ${RPM}" diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 010e79d..827ea35 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -235,7 +235,11 @@ jobs: libwayland-dev \ libxkbcommon-dev \ libxkbcommon-x11-dev \ - libfontconfig1-dev + libfontconfig1-dev \ + binutils \ + dpkg-dev \ + rpm \ + desktop-file-utils - name: Setup Rust uses: dtolnay/rust-toolchain@stable @@ -304,9 +308,11 @@ jobs: if: matrix.platform == 'ubuntu-22.04' run: | set -euo pipefail - VERSION="${{ needs.version-and-release.outputs.version }}" - mkdir -p dist - tar -C app/target/release -czf "dist/Grimoire_${VERSION}_${{ matrix.archive_suffix }}.tar.gz" Grimoire + chmod +x .github/scripts/package-linux.sh + .github/scripts/package-linux.sh \ + app/target/release/Grimoire \ + dist \ + "${{ matrix.archive_suffix }}" - name: Package Windows binary if: matrix.platform == 'windows-latest' diff --git a/Makefile b/Makefile index 80df8f9..0594a51 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ # make build # release binary for this machine # make build-mac # macOS .app and DMG (loads .env for signing) # make build-windows # Windows x64, on Windows -# make build-linux # Linux x64, on Linux +# make build-linux # Linux x64 tarball, deb, and rpm, on Linux CARGO ?= cargo @@ -43,7 +43,7 @@ help: @echo " make build-mac-arm macOS Apple Silicon disk image" @echo " make build-mac-intel macOS Intel disk image" @echo " make build-windows Windows x64 executable" - @echo " make build-linux Linux x64 executable" + @echo " make build-linux Linux x64 tarball, deb, and rpm" @echo " make test Run cargo test" @echo " make clean Remove Rust build outputs and dist/" @echo "" @@ -54,6 +54,8 @@ help: @echo "signing, set APPLE_SIGNING_IDENTITY in .env." @echo "" @echo "Linux builds need Wayland, xkbcommon, and fontconfig." + @echo "Linux packages also need binutils, dpkg-dev, and rpm" + @echo " (Ubuntu: sudo apt-get install binutils dpkg-dev rpm desktop-file-utils)." test: test-rust @@ -98,7 +100,13 @@ endif build-linux: ifeq ($(HOST_OS),Linux) + rustup target add $(LINUX_TARGET) $(CARGO) build --manifest-path app/Cargo.toml --release --bin Grimoire --target $(LINUX_TARGET) + chmod +x .github/scripts/package-linux.sh + .github/scripts/package-linux.sh \ + app/target/$(LINUX_TARGET)/release/Grimoire \ + dist \ + linux_x64 else $(error Linux builds require a Linux host (got $(HOST_OS))) endif diff --git a/README.md b/README.md index 28a81d3..2a82b24 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,20 @@ Cross-platform desktop eBook library manager. The window is a native [GPUI](https://gpui-kit.com/) app. The catalog is a local SQLite database with a Calibre-style layout on disk, plus USB/MTP sync for Kindles and other eReaders. -## Features (v0.1) +## Features - Create or open a library folder with `metadata.db` and book files on disk -- Import EPUB, PDF, AZW3, MOBI, TXT, and comics (CBZ/CBR) +- Import EPUB, PDF, AZW3, MOBI, TXT, and comics (CBZ/CBR), including by dropping files or folders on the library window - Extract metadata on import: title, authors, tags, series, description, identifiers, and cover (EPUB, PDF, MOBI/AZW3) - Fill missing metadata and cover art from Open Library (manual fetch, plus auto after sparse imports) -- Read EPUB, PDF, TXT, DRM-free MOBI, and comics (CBZ/CBR) in a separate window with saved progress (MOBI is converted to EPUB for display) +- Read EPUB, PDF, TXT, DRM-free MOBI, and comics (CBZ/CBR) in their own window with saved progress (MOBI is converted to EPUB for display) +- Reader controls for EPUB and TXT: font (publisher default, serif, sans, or OpenDyslexic), text size, and text colors (Native, Black and white, Inverted, Paper) +- Comic reader zoom, plus single-page or two-page layout - AZW3: import and Kindle send (Amazon store AZW3 is usually DRM-protected and is not opened in-app) - Convert formats in-app: EPUB → MOBI/AZW3, TXT → EPUB (no Calibre) -- Search (FTS5), edit metadata, delete books -- Detect connected Kindles and other eReaders over USB/MTP or mounted volumes +- Search (FTS5), filter by status, format, genre, and author, edit metadata, delete books +- Light, dark, or follow-system appearance +- Detect connected Kindles and other eReaders over USB/MTP or mounted volumes, with a chime on connect and disconnect - Convert EPUB to MOBI or AZW3 natively for Kindle USB send (no Calibre required) - Record transfer history per device @@ -38,12 +41,12 @@ Or use the Makefile: make build # release binary for this machine make build-mac # macOS disk image (loads .env for Apple signing if present) make build-windows # Windows x64 (Windows host) -make build-linux # Linux x64 (Linux host) +make build-linux # Linux x64 tarball, deb, and rpm (Linux host) ``` For a signed and notarized local macOS build, copy `.env.example` to `.env`, fill in the Apple values, then run `make build-mac`. See that file for variable notes. -Run `make help` for the full target list. Disk images land in `dist/`. The release binary is under `app/target/release/`. +Run `make help` for the full target list. Disk images and Linux packages land in `dist/`. The release binary is under `app/target/release/`. ## Test @@ -64,7 +67,7 @@ Every pull request runs the test suite. When a PR is **merged**, or when you run - Title starts with `MINOR` → minor bump (`1.2.3` → `1.3.0`) - Title starts with `PATCH`, or anything else → patch bump (`1.2.3` → `1.2.4`) 3. Creates a draft `vX.Y.Z` GitHub Release (notes from commit subjects since the previous tag) -4. Builds release binaries for macOS (arm64 + x64 disk images, signed and notarized), Linux x64, and Windows x64, then attaches them to that draft +4. Builds release binaries for macOS (arm64 + x64 disk images, signed and notarized), Linux x64 (tar.gz, deb, and rpm), and Windows x64, then attaches them to that draft 5. Publishes the release (immutable releases lock assets only after publish) Example titles: `MINOR add dark mode default`, `MAJOR redesign library schema`, `add filter sidebar` (patch). @@ -97,9 +100,13 @@ CI reads the signing identity from `APPLE_CERTIFICATE`, so do not set `APPLE_SIG Create the Developer ID certificate and the App Store Connect API key in [Apple Developer](https://developer.apple.com/account) and [App Store Connect](https://appstoreconnect.apple.com/access/integrations/api). Signed and notarized DMGs open normally; drag Grimoire into Applications. -### Windows +### Downloads + +Published releases include: -Windows releases are the `Grimoire.exe` binary. Choose the library folder in Settings. Grimoire writes `%APPDATA%\grimoire\config.json` and keeps that library path on later launches. +- **macOS:** signed and notarized disk images for Apple Silicon (`macos_arm64`) and Intel (`macos_x64`). Open the DMG and drag Grimoire into Applications. macOS 12 or later. +- **Windows:** a zip of `Grimoire.exe`. Unpack it and run the app. Choose the library folder in Settings. Grimoire writes `%APPDATA%\grimoire\config.json` and keeps that library path on later launches. +- **Linux:** x86-64 `.deb`, `.rpm`, and tar.gz (`linux_x64`). Install with `sudo apt install ./Grimoire_*_linux_x64.deb`, `sudo dnf install ./Grimoire_*_linux_x64.rpm`, or `sudo zypper install ./Grimoire_*_linux_x64.rpm`. Grimoire shows up in the application menu. The command is `grimoire`. The tar.gz is the binary on its own: unpack it and run `./Grimoire`. Builds need glibc 2.35 or newer (Ubuntu 22.04, Debian 12, Fedora 36, and later). Packages install the Wayland, libxkbcommon, fontconfig, and Vulkan loader dependencies. The tar.gz needs those libraries on the system already. ## Library layout @@ -149,7 +156,7 @@ Other USB devices that identify as an eReader or ebook device may appear as a ge ### Device detection -The Devices page checks for connected readers every few seconds. A notice appears when a reader connects or disconnects. Detection covers USB MTP devices and mounted volumes (Kobo, Kindle, reMarkable, and volume names that match known brands). +The Devices page checks for connected readers every few seconds. A notice appears when a reader connects or disconnects, and a short chime plays. Detection covers USB MTP devices and mounted volumes (Kobo, Kindle, reMarkable, and volume names that match known brands). ### Device library diff --git a/app/src/ui/icon.rs b/app/src/ui/icon.rs index a995a1f..c515b7d 100644 --- a/app/src/ui/icon.rs +++ b/app/src/ui/icon.rs @@ -6,6 +6,11 @@ const ICON_PNG: &[u8] = include_bytes!("../../icons/icon.png"); pub fn apply(options: &mut WindowOptions) { options.icon = Some(raster()); + // Matches the Linux desktop file id (Wayland app id and X11 WM_CLASS). + #[cfg(target_os = "linux")] + { + options.app_id = Some("com.boxingoctopus.grimoire".into()); + } } /// Dock and Cmd-Tab icon. The Windows executable icon is `icons/icon.ico`, embedded as resource 1. diff --git a/packaging/linux/com.boxingoctopus.grimoire.desktop b/packaging/linux/com.boxingoctopus.grimoire.desktop new file mode 100644 index 0000000..03b8f5d --- /dev/null +++ b/packaging/linux/com.boxingoctopus.grimoire.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Type=Application +Name=Grimoire +GenericName=eBook library +Comment=Manage a local eBook library and send books to eReaders +Exec=grimoire +Icon=com.boxingoctopus.grimoire +Terminal=false +Categories=Office; +Keywords=ebook;epub;pdf;mobi;kindle;library; +StartupWMClass=com.boxingoctopus.grimoire diff --git a/packaging/linux/com.boxingoctopus.grimoire.metainfo.xml b/packaging/linux/com.boxingoctopus.grimoire.metainfo.xml new file mode 100644 index 0000000..81c7a5f --- /dev/null +++ b/packaging/linux/com.boxingoctopus.grimoire.metainfo.xml @@ -0,0 +1,17 @@ + + + com.boxingoctopus.grimoire + Grimoire + eBook collection manager + CC0-1.0 + MPL-2.0 + Boxing Octopus Creative Engineering + +

Grimoire keeps a local library of EPUB, PDF, MOBI, AZW3, and comics, and sends books to Kindles and other eReaders.

+
+ com.boxingoctopus.grimoire.desktop + https://grimoirebooks.app + + + +
diff --git a/packaging/linux/grimoire.spec.in b/packaging/linux/grimoire.spec.in new file mode 100644 index 0000000..268eb98 --- /dev/null +++ b/packaging/linux/grimoire.spec.in @@ -0,0 +1,45 @@ +%global debug_package %{nil} +%global _build_id_links none +%global __os_install_post %{nil} + +Name: grimoire +Version: @VERSION@ +Release: 1 +Summary: eBook collection manager +License: MPL-2.0 +URL: https://grimoirebooks.app +BuildArch: @RPM_ARCH@ +Packager: Boxing Octopus Creative Engineering +# libwayland and libvulkan are dlopen'd, so RPM will not see them as NEEDED. +Requires: libwayland-client.so.0()(64bit) +Requires: libvulkan.so.1()(64bit) +Requires: hicolor-icon-theme + +%description +Grimoire keeps a local library of EPUB, PDF, MOBI, AZW3, and comics, +and sends books to Kindles and other eReaders. + +%install +rm -rf %{buildroot} +mkdir -p %{buildroot} +cp -a @PAYLOAD@/. %{buildroot}/ + +%post +if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database -q /usr/share/applications || : +fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + gtk-update-icon-cache -q /usr/share/icons/hicolor || : +fi + +%postun +if [ "$1" -eq 0 ]; then + if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database -q /usr/share/applications || : + fi + if command -v gtk-update-icon-cache >/dev/null 2>&1; then + gtk-update-icon-cache -q /usr/share/icons/hicolor || : + fi +fi + +%files -f @FILELIST@