diff --git a/.github/workflows/build-flatpak.yml b/.github/workflows/build-flatpak.yml new file mode 100644 index 0000000..971e4ea --- /dev/null +++ b/.github/workflows/build-flatpak.yml @@ -0,0 +1,30 @@ +# .github/workflows/build-flatpak.yml +name: reusable linux flatpak build + +on: + workflow_call: + outputs: + artifact_name: + description: "the name of the flatpak artifact" + value: ${{ jobs.build-flatpak.outputs.artifact_name }} + +jobs: + build-flatpak: + runs-on: ubuntu-22.04 + outputs: + artifact_name: ${{ steps.build_flatpak.outputs.bundle }} + container: + image: ghcr.io/flathub-infra/flatpak-github-actions:kde-6.10 + options: --privileged + steps: + - uses: actions/checkout@v4 + + - name: generate python flatpak dependencies + run: | + python3 flatpak/gen_deps.py + + - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 + id: build_flatpak + with: + bundle: meikipop.flatpak + manifest-path: flatpak/manifest.yml diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 91e6c89..2bc6850 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -15,6 +15,8 @@ jobs: build-linux: uses: ./.github/workflows/build-linux.yml + build-flatpak: + uses: ./.github/workflows/build-flatpak.yml build-macos: uses: ./.github/workflows/build-macos.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bd854fe..a9ebe32 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,11 +11,13 @@ jobs: uses: ./.github/workflows/build-windows.yml build-linux: uses: ./.github/workflows/build-linux.yml + build-flatpak: + uses: ./.github/workflows/build-flatpak.yml build-macos: uses: ./.github/workflows/build-macos.yml release: - needs: [ build-windows, build-linux, build-macos ] + needs: [ build-windows, build-linux, build-flatpak, build-macos ] runs-on: ubuntu-latest permissions: contents: write diff --git a/.gitignore b/.gitignore index 8a6b83c..5c2875d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,8 @@ dictionary.pkl .DS_Store src/meikipop.egg-info/ + +# Flatpak +bundle-repo +**.flatpak +python3-deps.json diff --git a/flatpak/gen_deps.py b/flatpak/gen_deps.py new file mode 100644 index 0000000..c50516a --- /dev/null +++ b/flatpak/gen_deps.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +""" +Generate python3-deps.json for a flatpak build. + +Uses pip's resolver to produce a dry-run report of all transitive deps. +Then emits a single-module flatpak manifest that installs the wheels/sdists offline. +Must be run on a flatpak target Linux x86_64 host. +""" + +import json +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +import tomllib + +PROJECT_PATH = Path("pyproject.toml") +OUT_PATH = Path("flatpak/python3-deps.json") + +WHEEL_NO_DEPS = ["pynput>=1.8"] +SDIST_PACKAGES = ["evdev"] + +PYTHON_VERSION = "3.13" +PLATFORMS = ["manylinux2014_x86_64", "manylinux_2_28_x86_64"] + +os.environ["PIP_DISABLE_PIP_VERSION_CHECK"] = "1" + + +def run_resolve(packages: list[str], extra_args: list[str]) -> list[dict]: + with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as tf: + report_path = Path(tf.name) + try: + cmd = [ + sys.executable, + "-m", + "pip", + "install", + "--dry-run", + "--ignore-installed", + "--python-version", + PYTHON_VERSION, + "--report", + str(report_path), + *extra_args, + *packages, + ] + subprocess.check_call(cmd) + return json.loads(report_path.read_text())["install"] + finally: + report_path.unlink(missing_ok=True) + + +def main() -> None: + platform_args = [f"--platform={p}" for p in PLATFORMS] + + deps = [ + dep + for dep in tomllib.loads(PROJECT_PATH.read_text())["project"]["dependencies"] + if not dep.startswith("pynput") + ] + resolved_packages = [ + *run_resolve(deps, ["--only-binary=:all:", *platform_args]), + *run_resolve( + WHEEL_NO_DEPS, ["--only-binary=:all:", "--no-deps", *platform_args] + ), + *run_resolve(SDIST_PACKAGES, ["--no-binary=:all:", "--no-deps"]), + ] + + seen = set() + sources = [] + for pkg in resolved_packages: + info = pkg["download_info"] + url = info["url"] + if url in seen: + continue + seen.add(url) + sources.append( + { + "type": "file", + "url": url, + "sha256": info["archive_info"]["hashes"]["sha256"], + } + ) + + OUT_PATH.write_text( + json.dumps( + { + "name": "python3-deps", + "buildsystem": "simple", + "build-commands": [ + ( + "pip3 install --no-index --find-links=. --prefix=/app --no-build-isolation --ignore-installed --no-deps *.whl *.tar.gz" + ) + ], + "cleanup": ["/bin", "/share/man"], + "sources": sources, + }, + indent=2, + ) + ) + + +if __name__ == "__main__": + main() diff --git a/flatpak/manifest.yml b/flatpak/manifest.yml new file mode 100644 index 0000000..c4bddc7 --- /dev/null +++ b/flatpak/manifest.yml @@ -0,0 +1,48 @@ +id: com.github.rtr46.meikipop +runtime: org.kde.Platform +runtime-version: "6.10" +sdk: org.kde.Sdk +base: com.riverbankcomputing.PyQt.BaseApp +base-version: "6.10" +command: "meikipop" +finish-args: + - "--socket=x11" + - "--socket=wayland" + - "--device=dri" + - "--share=ipc" + - "--share=network" + - --talk-name=org.kde.StatusNotifierWatcher + - "--filesystem=xdg-config/meikipop:rw" + - "--filesystem=xdg-config/screen_ai:rw" + - "--filesystem=xdg-data/meikipop:rw" +modules: + - name: pycairo + buildsystem: meson + sources: + - type: archive + url: https://files.pythonhosted.org/packages/22/d9/1728840a22a4ef8a8f479b9156aa2943cd98c3907accd3849fb0d5f82bfd/pycairo-1.29.0.tar.gz + sha256: f3f7fde97325cae80224c09f12564ef58d0d0f655da0e3b040f5807bd5bd3142 + - name: pygobject + buildsystem: meson + sources: + - type: archive + url: https://download.gnome.org/sources/pygobject/3.56/pygobject-3.56.1.tar.gz + sha256: 2ec1cc8c55c7ffeebb97e58a9bba7aa1e74611f1173628084685446804a8881a + - python3-deps.json + - name: meikipop + buildsystem: simple + build-commands: + - | + python3 - << 'EOF' + from PIL import Image + img = Image.open("src/meikipop/resources/icon.ico") + img.save("icon.png") + EOF + - pip3 install --no-index --find-links=. --prefix=/app --no-build-isolation + --no-deps . + - install -Dm644 flatpak/meikipop.desktop /app/share/applications/com.github.rtr46.meikipop.desktop + - install -Dm644 flatpak/metainfo.xml /app/share/metainfo/com.github.rtr46.meikipop.metainfo.xml + - install -Dm644 icon.png /app/share/icons/hicolor/256x256/apps/com.github.rtr46.meikipop.png + sources: + - type: dir + path: .. diff --git a/flatpak/meikipop.desktop b/flatpak/meikipop.desktop new file mode 100644 index 0000000..27d4d22 --- /dev/null +++ b/flatpak/meikipop.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=Meikipop +Comment=Japanese OCR Popup Dictionary +Exec=meikipop +Icon=com.github.rtr46.meikipop +Terminal=false +Type=Application +Categories=Education;Languages;Utility; +Keywords=Japanese;OCR;Dictionary;Popup; diff --git a/flatpak/metainfo.xml b/flatpak/metainfo.xml new file mode 100644 index 0000000..df7ba8f --- /dev/null +++ b/flatpak/metainfo.xml @@ -0,0 +1,32 @@ + + + com.github.rtr46.meikipop + + meikipop + Instantly look up Japanese words anywhere on your screen + + + rtr46 + + + https://github.com/rtr46/meikipop + + + CC0-1.0 + GPL-3.0-or-later + + +

+ Instantly look up japanese words anywhere on your screen. Meikipop uses optical character recognition (ocr) to read text from websites, games, scanned manga, or even hard-coded video subtitles, giving you effortless dictionary lookups with the press of a key (or even without)! +

+
+ + com.github.rtr46.meikipop.desktop + + + + https://github.com/user-attachments/assets/a1834197-3059-438c-a2dc-716e8ec9078f + + + +
diff --git a/src/meikipop/screenshot/wayland_mss_shim.py b/src/meikipop/screenshot/wayland_mss_shim.py index 5937851..5bd826e 100644 --- a/src/meikipop/screenshot/wayland_mss_shim.py +++ b/src/meikipop/screenshot/wayland_mss_shim.py @@ -10,6 +10,8 @@ gi.require_version('Gst', '1.0') from gi.repository import GLib, Gst, Gio +from meikipop.utils.paths import paths + import mss as real_mss from mss.exception import ScreenShotError from mss.screenshot import ScreenShot, Size @@ -20,7 +22,7 @@ screencast = None screencast_lock = threading.Lock() -token_file = Path('~/.cache/.ocr_screencapture_token').expanduser() # todo this should probably use utils/path.py +token_file = Path(paths.cache_dir) / '.ocr_screencapture_token' persist_token = str(uuid.UUID(int=0)) if token_file.exists():