# 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: """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.""" from aqt import mw launcher = launcher_executable() assert launcher trigger_launcher_run() with contextlib.suppress(ResourceWarning): env = os.environ.copy() env["ANKI_LAUNCHER_WANT_TERMINAL"] = "1" # fixes a bug where launcher fails to appear if opening it # straight after updating if "GNOME_TERMINAL_SCREEN" in env: del env["GNOME_TERMINAL_SCREEN"] creationflags = 0 if sys.platform == "win32": creationflags = ( subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS ) # On Windows 10, 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()