mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00

While something we probably don't want to encourage much of, this may enable some previously-unshared add-ons. https://forums.ankiweb.net/t/bundling-numpy-in-an-add-on/62669/5 The 'uv add' command is transaction, so if an add-on tries to inject incompatible dependencies into the environment, the venv will be left as-is. And each Anki upgrade/downgrade resets the requirements, so the requested packages shouldn't cause errors down the line. Sample add-on: import subprocess from aqt import mw from aqt.operations import QueryOp from aqt.qt import QAction from aqt.utils import showInfo def ensure_spacy(col): print("trying to import spacy") try: import spacy print("successful import") return except Exception as e: print("error importing:", e) print("attempting add") try: from aqt.package import add_python_requirements as add except Exception as e: raise Exception(f"package unavailable, can't install: {e}") # be explicit about version, or Anki beta users will get # a beta wheel that may break (success, output) = add(["spacy==3.8.7", "https://github.com/explosion/spacy-models/releases/download/ko_core_news_sm-3.8.0/ko_core_news_sm-3.8.0-py3-none-any.whl"]) if not success: raise Exception(f"adding failed: {output}") print("success") # alterantively: # from aqt.package import venv_binary # subprocess.run([venv_binary("spacy"), "download", "ko_core_news_sm"], check=True) # print("model added") # large packages will freeze for a while on first import on macOS import spacy print("spacy import successful") def activate_spacy(): def on_success(res): mw.progress.single_shot(1000, lambda: showInfo("Spacy installed")) QueryOp(parent=mw, op=ensure_spacy, success=on_success).with_progress("Installing spacy...").run_in_background() action = QAction("Activate Spacy", mw) action.triggered.connect(activate_spacy) mw.form.menuTools.addAction(action)
168 lines
4.2 KiB
Python
168 lines
4.2 KiB
Python
# Copyright: Ankitects Pty Ltd and contributors
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
"""Helpers for the packaged version of Anki."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from anki.utils import is_mac, is_win
|
|
|
|
|
|
# ruff: noqa: F401
|
|
def first_run_setup() -> None:
|
|
"""Code run the first time after install/upgrade.
|
|
|
|
Currently, we just import our main libraries and invoke
|
|
mpv/lame on macOS, which is slow on the first run, and doing
|
|
it this way shows progress being made.
|
|
"""
|
|
|
|
if not is_mac:
|
|
return
|
|
|
|
# Import anki_audio first and spawn commands
|
|
import anki_audio
|
|
|
|
audio_pkg_path = Path(anki_audio.__file__).parent
|
|
|
|
# Start mpv and lame commands concurrently
|
|
processes = []
|
|
for cmd_name in ["mpv", "lame"]:
|
|
cmd_path = audio_pkg_path / cmd_name
|
|
proc = subprocess.Popen(
|
|
[str(cmd_path), "--version"],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
processes.append(proc)
|
|
|
|
# Continue with other imports while commands run
|
|
import concurrent.futures
|
|
|
|
import bs4
|
|
import flask
|
|
import flask_cors
|
|
import markdown
|
|
import PyQt6.QtCore
|
|
import PyQt6.QtGui
|
|
import PyQt6.QtNetwork
|
|
import PyQt6.QtQuick
|
|
import PyQt6.QtWebChannel
|
|
import PyQt6.QtWebEngineCore
|
|
import PyQt6.QtWebEngineWidgets
|
|
import PyQt6.QtWidgets
|
|
import PyQt6.sip
|
|
import requests
|
|
import waitress
|
|
|
|
import anki.collection
|
|
|
|
from . import _macos_helper
|
|
|
|
# Wait for both commands to complete
|
|
for proc in processes:
|
|
proc.wait()
|
|
|
|
|
|
def uv_binary() -> str | None:
|
|
"""Return the path to the uv binary."""
|
|
return os.environ.get("ANKI_LAUNCHER_UV")
|
|
|
|
|
|
def launcher_root() -> str | None:
|
|
"""Return the path to the launcher root directory (AnkiProgramFiles)."""
|
|
return os.environ.get("UV_PROJECT")
|
|
|
|
|
|
def venv_binary(cmd: str) -> str | None:
|
|
"""Return the path to a binary in the launcher's venv."""
|
|
root = launcher_root()
|
|
if not root:
|
|
return None
|
|
|
|
root_path = Path(root)
|
|
if is_win:
|
|
binary_path = root_path / ".venv" / "Scripts" / cmd
|
|
else:
|
|
binary_path = root_path / ".venv" / "bin" / cmd
|
|
|
|
return str(binary_path)
|
|
|
|
|
|
def add_python_requirements(reqs: list[str]) -> tuple[bool, str]:
|
|
"""Add Python requirements to the launcher venv using uv add.
|
|
|
|
Returns (success, output)"""
|
|
|
|
binary = uv_binary()
|
|
if not binary:
|
|
return (False, "Not in packaged build.")
|
|
|
|
uv_cmd = [binary, "add"] + reqs
|
|
result = subprocess.run(uv_cmd, capture_output=True, text=True, check=False)
|
|
|
|
if result.returncode == 0:
|
|
root = launcher_root()
|
|
if root:
|
|
sync_marker = Path(root) / ".sync_complete"
|
|
sync_marker.touch()
|
|
|
|
return (True, result.stdout)
|
|
else:
|
|
return (False, result.stderr)
|
|
|
|
|
|
def launcher_executable() -> str | None:
|
|
"""Return the path to the Anki launcher executable."""
|
|
return os.getenv("ANKI_LAUNCHER")
|
|
|
|
|
|
def trigger_launcher_run() -> None:
|
|
"""Bump the mtime on pyproject.toml in the local data directory to trigger an update on next run."""
|
|
try:
|
|
root = launcher_root()
|
|
if not root:
|
|
return
|
|
|
|
pyproject_path = Path(root) / "pyproject.toml"
|
|
|
|
if pyproject_path.exists():
|
|
# Touch the file to update its mtime
|
|
pyproject_path.touch()
|
|
except Exception as e:
|
|
print(e)
|
|
|
|
|
|
def update_and_restart() -> None:
|
|
"""Update and restart Anki using the launcher."""
|
|
from aqt import mw
|
|
|
|
launcher = launcher_executable()
|
|
assert launcher
|
|
|
|
trigger_launcher_run()
|
|
|
|
with contextlib.suppress(ResourceWarning):
|
|
env = os.environ.copy()
|
|
creationflags = 0
|
|
if sys.platform == "win32":
|
|
creationflags = (
|
|
subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
|
|
)
|
|
subprocess.Popen(
|
|
[launcher],
|
|
start_new_session=True,
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
env=env,
|
|
creationflags=creationflags,
|
|
)
|
|
|
|
mw.app.quit()
|