Add a hidden env var to preload our libs and audio helpers on macOS

This commit is contained in:
Damien Elmes 2025-06-14 17:28:44 +07:00
parent 3d69083f67
commit 726318f016
6 changed files with 67 additions and 137 deletions

View file

@ -3,12 +3,18 @@
from __future__ import annotations
import os
import atexit
import logging
import sys
from collections.abc import Callable
from typing import TYPE_CHECKING, Any, Union, cast
if "ANKI_FIRST_RUN" in os.environ:
from .package import first_run_setup
first_run_setup()
try:
import pip_system_certs.wrapt_requests
except ModuleNotFoundError:
@ -32,18 +38,9 @@ if "--syncserver" in sys.argv:
from anki.syncserver import run_sync_server
from anki.utils import is_mac
from .package import _fix_protobuf_path
if is_mac and getattr(sys, "frozen", False):
_fix_protobuf_path()
# does not return
run_sync_server()
from .package import packaged_build_setup
packaged_build_setup()
import argparse
import builtins
import cProfile
@ -270,12 +267,6 @@ def setupLangAndBackend(
# load qt translations
_qtrans = QTranslator()
if is_mac and getattr(sys, "frozen", False):
qt_dir = os.path.join(sys.prefix, "../Resources/qt_translations")
else:
if qtmajor == 5:
qt_dir = QLibraryInfo.location(QLibraryInfo.TranslationsPath) # type: ignore
else:
qt_dir = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)
qt_lang = lang.replace("-", "_")
if _qtrans.load(f"qtbase_{qt_lang}", qt_dir):
@ -607,14 +598,13 @@ def _run(argv: list[str] | None = None, exec: bool = True) -> AnkiApp | None:
profiler = cProfile.Profile()
profiler.enable()
packaged = getattr(sys, "frozen", False)
x11_available = os.getenv("DISPLAY")
wayland_configured = qtmajor > 5 and (
os.getenv("QT_QPA_PLATFORM") == "wayland" or os.getenv("WAYLAND_DISPLAY")
)
wayland_forced = os.getenv("ANKI_WAYLAND")
if (packaged or is_gnome) and wayland_configured:
if is_gnome and wayland_configured:
if wayland_forced or not x11_available:
# Work around broken fractional scaling in Wayland
# https://bugreports.qt.io/browse/QTBUG-113574

View file

@ -14,9 +14,6 @@ import aqt.utils
class _MacOSHelper:
def __init__(self) -> None:
if getattr(sys, "frozen", False):
path = os.path.join(sys.prefix, "libankihelper.dylib")
else:
path = os.path.join(
aqt.utils.aqt_data_folder(), "lib", "libankihelper.dylib"
)

View file

@ -252,12 +252,6 @@ def _handle_local_file_request(request: LocalFileRequest) -> Response:
def _builtin_data(path: str) -> bytes:
"""Return data from file in aqt/data folder.
Path must use forward slash separators."""
# packaged build?
if getattr(sys, "frozen", False):
reader = aqt.__loader__.get_resource_reader("_aqt") # type: ignore
with reader.open_resource(path) as f:
return f.read()
else:
full_path = aqt_data_path() / ".." / path
return full_path.read_bytes()

View file

@ -4,94 +4,51 @@
"""Helpers for the packaged version of Anki."""
from __future__ import annotations
import os
import sys
from pathlib import Path
import subprocess
from anki.utils import is_mac
def first_run_setup() -> None:
"""Code run the first time after install/upgrade.
def _fix_pywin32() -> None:
# extend sys.path with .pth files
import site
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.
"""
site.addsitedir(sys.path[0])
# use updated sys.path to locate dll folder and add it to path
path = sys.path[-1]
path = path.replace("Pythonwin", "pywin32_system32")
os.environ["PATH"] += ";" + path
# import Python modules from .dll files
import importlib.machinery
for name in "pythoncom", "pywintypes":
filename = os.path.join(path, name + "39.dll")
loader = importlib.machinery.ExtensionFileLoader(name, filename)
spec = importlib.machinery.ModuleSpec(name=name, loader=loader, origin=filename)
_mod = importlib._bootstrap._load(spec) # type: ignore
def _patch_pkgutil() -> None:
"""Teach pkgutil.get_data() how to read files from in-memory resources.
This is required for jsonschema."""
import importlib
import pkgutil
def get_data_custom(package: str, resource: str) -> bytes | None:
try:
module = importlib.import_module(package)
reader = module.__loader__.get_resource_reader(package) # type: ignore
with reader.open_resource(resource) as f:
return f.read()
except Exception:
return None
pkgutil.get_data = get_data_custom
def _patch_certifi() -> None:
"""Tell certifi (and thus requests) to use a file in our package folder.
By default it creates a copy of the data in a temporary folder, which then gets
cleaned up by macOS's temp file cleaner."""
import certifi
def where() -> str:
prefix = Path(sys.prefix)
if sys.platform == "darwin":
path = prefix / "../Resources/certifi/cacert.pem"
else:
path = prefix / "lib" / "certifi" / "cacert.pem"
return str(path)
certifi.where = where
def _fix_protobuf_path() -> None:
sys.path.append(str(Path(sys.prefix) / "../Resources"))
def packaged_build_setup() -> None:
if not getattr(sys, "frozen", False):
if not is_mac:
return
print("Initial setup...")
def _dot():
print(".", flush=True, end="")
if sys.platform == "win32":
_fix_pywin32()
elif sys.platform == "darwin":
_fix_protobuf_path()
_dot()
import anki.collection
_dot()
import PyQt6.sip
_dot()
import PyQt6.QtCore
_dot()
import PyQt6.QtGui
_dot()
import PyQt6.QtNetwork
_dot()
import PyQt6.QtQuick
_dot()
import PyQt6.QtWebChannel
_dot()
import PyQt6.QtWebEngineCore
_dot()
import PyQt6.QtWebEngineWidgets
_dot()
import PyQt6.QtWidgets
_patch_pkgutil()
_patch_certifi()
import anki_audio
audio_pkg_path = Path(anki_audio.__file__).parent
# escape hatch for debugging issues with packaged build startup
if os.getenv("ANKI_STARTUP_REPL"):
# mypy incorrectly thinks this does not exist on Windows
is_tty = os.isatty(sys.stdin.fileno()) # type: ignore
if is_tty:
import code
code.InteractiveConsole().interact()
sys.exit(0)
# Invoke mpv and lame
cmd = [Path(""), "--version"]
for cmd_name in ["mpv", "lame"]:
_dot()
cmd[0] = audio_pkg_path / cmd_name
subprocess.run(cmd, check=True, capture_output=True)

View file

@ -87,15 +87,6 @@ if TYPE_CHECKING:
def aqt_data_path() -> Path:
# packaged?
if getattr(sys, "frozen", False):
prefix = Path(sys.prefix)
path = prefix / "lib/_aqt/data"
if path.exists():
return path
else:
return prefix / "../Resources/_aqt/data"
else:
import _aqt.colors
data_folder = Path(inspect.getfile(_aqt.colors)).with_name("data")
@ -1207,12 +1198,11 @@ def supportText() -> str:
platname = platform.platform()
return """\
Anki {} {} {}
Anki {} {}
Python {} Qt {} PyQt {}
Platform: {}
""".format(
version_with_build(),
"(src)" if not getattr(sys, "frozen", False) else "",
"(ao)" if mw.addonManager.dirty else "",
platform.python_version(),
qVersion(),

View file

@ -87,7 +87,10 @@ fn main() {
// Pre-validate by running --version to trigger any Gatekeeper checks
let anki_bin = uv_install_root.join(".venv/bin/anki");
println!("\n\x1B[1mThis may take a few minutes. Please wait...\x1B[0m");
let _ = Command::new(&anki_bin).arg("--version").output();
let _ = Command::new(&anki_bin)
.env("ANKI_FIRST_RUN", "1")
.arg("--version")
.status();
// Then launch the binary as detached subprocess so the terminal can close
let child = Command::new(&anki_bin)
@ -98,7 +101,6 @@ fn main() {
.spawn()
.unwrap();
std::mem::forget(child);
println!("Anki launched successfully");
} else {
// If venv already existed, exec as normal
println!(