Skip to content
Merged
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
241 changes: 241 additions & 0 deletions .github/scripts/package-linux.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
#!/usr/bin/env bash
# Build a Grimoire tarball, .deb, and .rpm from a release binary.
# Usage: package-linux.sh <binary> <output-dir> <arch-label>
#
# 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 <ryan.draga@boxingoctop.us>"
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" <<EOF
Source: grimoire
Maintainer: ${MAINTAINER}

Package: grimoire
Architecture: ${DEB_ARCH}
Description: eBook collection manager
placeholder
EOF
SHLIB_ERR="${STAGE}/shlibdeps.err"
SHLIBS="$(
cd "${DEB_ROOT}"
dpkg-shlibdeps -O usr/bin/grimoire 2>"${SHLIB_ERR}"
)" || {
cat "${SHLIB_ERR}" >&2
exit 1
}
# Expected when the binary is not staged under debian/<package>/.
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" <<EOF
Package: grimoire
Version: ${VERSION}
Section: utils
Priority: optional
Architecture: ${DEB_ARCH}
Maintainer: ${MAINTAINER}
Homepage: ${HOMEPAGE}
Installed-Size: ${INSTALLED_SIZE}
Depends: ${DEPENDS}
Recommends: fonts-liberation | fonts-dejavu-core
Description: eBook collection manager
Grimoire keeps a local library of EPUB, PDF, MOBI, AZW3, and comics,
and sends books to Kindles and other eReaders.
EOF

cat > "${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}"
14 changes: 10 additions & 4 deletions .github/workflows/ci-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down
12 changes: 10 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 ""
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
29 changes: 18 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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).
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions app/src/ui/icon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Loading