mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
192 lines
5.2 KiB
Python
192 lines
5.2 KiB
Python
# Copyright: Ankitects Pty Ltd and contributors
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from anki.utils import pointVersion
|
|
from aqt import mw
|
|
from aqt.qt import QAction
|
|
from aqt.utils import askUser, is_mac, is_win, showInfo
|
|
|
|
|
|
def launcher_executable() -> str | None:
|
|
"""Return the path to the Anki launcher executable."""
|
|
return os.getenv("ANKI_LAUNCHER")
|
|
|
|
|
|
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 trigger_launcher_run() -> None:
|
|
"""Create a trigger file to request launcher UI on next run."""
|
|
try:
|
|
root = launcher_root()
|
|
if not root:
|
|
return
|
|
|
|
trigger_path = Path(root) / ".want-launcher"
|
|
trigger_path.touch()
|
|
except Exception as e:
|
|
print(e)
|
|
|
|
|
|
def update_and_restart() -> None:
|
|
"""Update and restart Anki using the launcher."""
|
|
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
|
|
)
|
|
# On Windows, changing the handles breaks ANSI display
|
|
io = None if sys.platform == "win32" else subprocess.DEVNULL
|
|
|
|
subprocess.Popen(
|
|
[launcher],
|
|
start_new_session=True,
|
|
stdin=io,
|
|
stdout=io,
|
|
stderr=io,
|
|
env=env,
|
|
creationflags=creationflags,
|
|
)
|
|
|
|
mw.app.quit()
|
|
|
|
|
|
def confirm_then_upgrade():
|
|
if not askUser("Change to a different Anki version?"):
|
|
return
|
|
update_and_restart()
|
|
|
|
|
|
# return modified command array that points to bundled command, and return
|
|
# required environment
|
|
def _packagedCmd(cmd: list[str]) -> tuple[Any, dict[str, str]]:
|
|
cmd = cmd[:]
|
|
env = os.environ.copy()
|
|
# keep LD_LIBRARY_PATH when in snap environment
|
|
if "LD_LIBRARY_PATH" in env and "SNAP" not in env:
|
|
del env["LD_LIBRARY_PATH"]
|
|
|
|
# Try to find binary in anki-audio package for Windows/Mac
|
|
if is_win or is_mac:
|
|
try:
|
|
import anki_audio
|
|
|
|
audio_pkg_path = Path(anki_audio.__file__).parent
|
|
if is_win:
|
|
packaged_path = audio_pkg_path / (cmd[0] + ".exe")
|
|
else: # is_mac
|
|
packaged_path = audio_pkg_path / cmd[0]
|
|
|
|
if packaged_path.exists():
|
|
cmd[0] = str(packaged_path)
|
|
return cmd, env
|
|
except ImportError:
|
|
# anki-audio not available, fall back to old behavior
|
|
pass
|
|
|
|
packaged_path = Path(sys.prefix) / cmd[0]
|
|
if packaged_path.exists():
|
|
cmd[0] = str(packaged_path)
|
|
|
|
return cmd, env
|
|
|
|
|
|
def on_addon_config():
|
|
showInfo(
|
|
"This add-on is automatically added when installing older Anki versions, so that they work with the launcher. You can remove it if you wish."
|
|
)
|
|
|
|
|
|
def setup():
|
|
mw.addonManager.setConfigAction(__name__, on_addon_config)
|
|
|
|
if pointVersion() >= 250600:
|
|
return
|
|
if not launcher_executable():
|
|
return
|
|
|
|
# Add action to tools menu
|
|
action = QAction("Upgrade/Downgrade", mw)
|
|
action.triggered.connect(confirm_then_upgrade)
|
|
mw.form.menuTools.addAction(action)
|
|
|
|
# Monkey-patch audio tools to use anki-audio
|
|
if is_win or is_mac:
|
|
import aqt
|
|
import aqt.sound
|
|
|
|
aqt.sound._packagedCmd = _packagedCmd
|
|
|
|
# Inject launcher functions into launcher module
|
|
import aqt.package
|
|
|
|
aqt.package.launcher_executable = launcher_executable
|
|
aqt.package.update_and_restart = update_and_restart
|
|
aqt.package.trigger_launcher_run = trigger_launcher_run
|
|
aqt.package.uv_binary = uv_binary
|
|
aqt.package.launcher_root = launcher_root
|
|
aqt.package.venv_binary = venv_binary
|
|
aqt.package.add_python_requirements = add_python_requirements
|
|
|
|
|
|
setup()
|