diff --git a/packaging/appimage/AppImageBuilder.yml b/packaging/appimage/AppImageBuilder.yml new file mode 100644 index 0000000..33273ec --- /dev/null +++ b/packaging/appimage/AppImageBuilder.yml @@ -0,0 +1,91 @@ +version: 1 + +AppDir: + path: !ENV '${SCANSLICER_APPDIR}' + app_info: + id: scanslicer + name: ScanSlicer + icon: scanslicer + version: "1.0.0" + exec: usr/bin/python3 + exec_args: "-m scanslicer $@" + + apt: + arch: amd64 + allow_unauthenticated: false + sources: + - sourceline: "deb https://archive.ubuntu.com/ubuntu/ noble main restricted universe multiverse" + key_url: "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x871920D1991BC93C" + include: + - python3 + - python3-pip + - libdbus-1-3 + - libfontconfig1 + - libegl1 + - libgl1 + - libglib2.0-0 + - libglib2.0-bin + - libsm6 + - libwayland-client0 + - libwayland-cursor0 + - libwayland-egl1 + - libxkbcommon0 + - libx11-xcb1 + - libxcb-cursor0 + - libgdk-pixbuf2.0-bin + - libgtk-3-bin + + files: + include: + - /usr/bin/python3 + - /usr/bin/pip3 + - /usr/lib + - /usr/share + exclude: + - usr/share/man + - usr/share/doc/*/README.* + - usr/share/doc/*/changelog.* + - usr/share/doc/*/NEWS.* + - usr/share/doc/*/TODO.* + - usr/lib/x86_64-linux-gnu/libQt6*.so* + - usr/lib/x86_64-linux-gnu/qt6/** + + runtime: + env: + APPDIR: "${APPDIR}" + PYTHONHOME: "${APPDIR}/usr" + PYTHONNOUSERSITE: "1" + QT_PLUGIN_PATH: "${APPDIR}/usr/local/lib/python3.12/dist-packages/PySide6/Qt/plugins" + LD_LIBRARY_PATH: "${APPDIR}/usr/local/lib/python3.12/dist-packages/PySide6/Qt/lib:${APPDIR}/usr/local/lib/python3.12/dist-packages/opencv_python.libs:${APPDIR}/usr/lib:${APPDIR}/usr/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH}" + PATH: "${APPDIR}/usr/bin:${PATH}" + + after_bundle: | + APPDIR_PATH="${APPDIR:-${SCANSLICER_APPDIR:-}}" + if [ -z "${APPDIR_PATH}" ]; then + echo "AppDir path is not set (SCANSLICER_APPDIR/APPDIR)." >&2 + exit 1 + fi + mkdir -p "${APPDIR_PATH}/usr/share/applications" + mkdir -p "${APPDIR_PATH}/usr/share/icons" + mkdir -p "${APPDIR_PATH}/usr/share/pixmaps" + mkdir -p "${APPDIR_PATH}/icons" + mkdir -p "${APPDIR_PATH}/pixmaps" + cp "${SCANSLICER_ROOT}/packaging/appimage/scanslicer.desktop" "${APPDIR_PATH}/usr/share/applications/scanslicer.desktop" + cp "${SCANSLICER_ROOT}/packaging/appimage/scanslicer.svg" "${APPDIR_PATH}/usr/share/icons/scanslicer.svg" + cp "${SCANSLICER_ROOT}/packaging/appimage/scanslicer.svg" "${APPDIR_PATH}/usr/share/pixmaps/scanslicer.svg" + rsvg-convert -w 256 -h 256 "${SCANSLICER_ROOT}/packaging/appimage/scanslicer.svg" > "${APPDIR_PATH}/usr/share/icons/scanslicer.png" + cp "${APPDIR_PATH}/usr/share/icons/scanslicer.png" "${APPDIR_PATH}/usr/share/pixmaps/scanslicer.png" + cp "${APPDIR_PATH}/usr/share/icons/scanslicer.png" "${APPDIR_PATH}/icons/scanslicer.png" + cp "${APPDIR_PATH}/usr/share/icons/scanslicer.png" "${APPDIR_PATH}/pixmaps/scanslicer.png" + cp "${APPDIR_PATH}/usr/share/icons/scanslicer.png" "${APPDIR_PATH}/.DirIcon" + test -f "${APPDIR_PATH}/usr/share/icons/scanslicer.png" + test -f "${APPDIR_PATH}/usr/share/pixmaps/scanslicer.png" + test -f "${APPDIR_PATH}/icons/scanslicer.png" + test -f "${APPDIR_PATH}/pixmaps/scanslicer.png" + test -f "${APPDIR_PATH}/.DirIcon" + rm -rf "${APPDIR_PATH}/usr/lib/x86_64-linux-gnu/qt6" + rm -f "${APPDIR_PATH}"/usr/lib/x86_64-linux-gnu/libQt6*.so* + python3 -m pip install --no-cache-dir --upgrade --ignore-installed --prefix=/usr --root="${APPDIR_PATH}" -r "${SCANSLICER_REQUIREMENTS}" + +AppImage: + arch: x86_64 diff --git a/packaging/appimage/Dockerfile b/packaging/appimage/Dockerfile new file mode 100644 index 0000000..4de3426 --- /dev/null +++ b/packaging/appimage/Dockerfile @@ -0,0 +1,34 @@ +FROM ubuntu:24.04 + +COPY --from=ghcr.io/astral-sh/uv:0.10.2 /uv /uvx /usr/local/bin/ + +ENV DEBIAN_FRONTEND=noninteractive \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + binutils \ + ca-certificates \ + desktop-file-utils \ + fakeroot \ + file \ + libgdk-pixbuf2.0-bin \ + libglib2.0-bin \ + libgtk-3-bin \ + libfuse2 \ + librsvg2-bin \ + patchelf \ + python3 \ + python3-pip \ + python3-setuptools \ + squashfs-tools \ + strace \ + xz-utils \ + zsync \ + && rm -rf /var/lib/apt/lists/* + +RUN python3 -m pip install --no-cache-dir --break-system-packages \ + "packaging<21" \ + appimage-builder + +WORKDIR /work diff --git a/packaging/appimage/build-appimage-docker.sh b/packaging/appimage/build-appimage-docker.sh new file mode 100755 index 0000000..a6f566b --- /dev/null +++ b/packaging/appimage/build-appimage-docker.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +IMAGE_NAME="${APPIMAGE_DOCKER_IMAGE:-scanslicer-appimage-builder}" +HOST_UID="${HOST_UID:-$(id -u)}" +HOST_GID="${HOST_GID:-$(id -g)}" + +if ! command -v docker >/dev/null 2>&1; then + echo "Missing required command: docker" >&2 + exit 1 +fi + +docker build \ + -f "${ROOT_DIR}/packaging/appimage/Dockerfile" \ + -t "${IMAGE_NAME}" \ + "${ROOT_DIR}" + +docker run --rm \ + -e HOST_UID="${HOST_UID}" \ + -e HOST_GID="${HOST_GID}" \ + -v "${ROOT_DIR}:/work" \ + -w /work \ + --entrypoint bash \ + "${IMAGE_NAME}" \ + -lc 'set -euo pipefail; bash packaging/appimage/build-appimage.sh; if [ -d /work/appimage ]; then chown -R "${HOST_UID}:${HOST_GID}" /work/appimage; fi' + +echo "Docker AppImage build complete. Output in ${ROOT_DIR}/appimage/dist" diff --git a/packaging/appimage/build-appimage.sh b/packaging/appimage/build-appimage.sh new file mode 100755 index 0000000..4fe4a9e --- /dev/null +++ b/packaging/appimage/build-appimage.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +APPIMAGE_ROOT="${ROOT_DIR}/appimage" +BUILD_DIR="${APPIMAGE_ROOT}/build" +APPDIR="${BUILD_DIR}/AppDir" +DIST_DIR="${APPIMAGE_ROOT}/dist" + +APPIMAGE_BUILDER_BIN="${APPIMAGE_BUILDER_BIN:-appimage-builder}" + +for cmd in uv "${APPIMAGE_BUILDER_BIN}"; do + if ! command -v "${cmd}" >/dev/null 2>&1; then + echo "Missing required command: ${cmd}" >&2 + echo "See docs/appimage.md for setup instructions." >&2 + exit 1 + fi +done + +mkdir -p "${BUILD_DIR}" "${DIST_DIR}" +rm -rf "${APPDIR}" +mkdir -p "${APPDIR}" + +cd "${ROOT_DIR}" +uv export \ + --frozen \ + --no-dev \ + --no-emit-project \ + --no-hashes \ + --format requirements-txt \ + --output-file "${BUILD_DIR}/requirements.txt" +uv build --wheel --out-dir "${BUILD_DIR}" + +WHEEL_PATH="$(ls -1 "${BUILD_DIR}"/scanslicer-*.whl | head -n 1)" +if [[ -z "${WHEEL_PATH}" ]]; then + echo "Could not find built wheel in ${BUILD_DIR}" >&2 + exit 1 +fi +printf "%s\n" "${WHEEL_PATH}" >> "${BUILD_DIR}/requirements.txt" + +export SCANSLICER_ROOT="${ROOT_DIR}" +export SCANSLICER_APPDIR="${APPDIR}" +export SCANSLICER_REQUIREMENTS="${BUILD_DIR}/requirements.txt" + +cd "${BUILD_DIR}" +"${APPIMAGE_BUILDER_BIN}" --recipe "${ROOT_DIR}/packaging/appimage/AppImageBuilder.yml" + +mkdir -p "${DIST_DIR}" +find "${BUILD_DIR}" -maxdepth 1 -type f \( -name "*.AppImage" -o -name "*.zsync" \) -exec mv -f {} "${DIST_DIR}/" \; + +echo "AppImage build complete. Output in ${DIST_DIR}" diff --git a/packaging/appimage/scanslicer.desktop b/packaging/appimage/scanslicer.desktop new file mode 100644 index 0000000..0c94bca --- /dev/null +++ b/packaging/appimage/scanslicer.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Name=ScanSlicer +Comment=Detect, straighten and save photos from flatbed scans +Exec=scanslicer %F +Icon=scanslicer +Terminal=false +Categories=Graphics;Photography; +StartupWMClass=ScanSlicer diff --git a/packaging/appimage/scanslicer.svg b/packaging/appimage/scanslicer.svg new file mode 100644 index 0000000..2eea769 --- /dev/null +++ b/packaging/appimage/scanslicer.svg @@ -0,0 +1,7 @@ + + + + + + +