mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
Add a hidden env var to preload our libs and audio helpers on macOS
This commit is contained in:
parent
3d69083f67
commit
726318f016
6 changed files with 67 additions and 137 deletions
|
@ -3,12 +3,18 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import atexit
|
import atexit
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from typing import TYPE_CHECKING, Any, Union, cast
|
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:
|
try:
|
||||||
import pip_system_certs.wrapt_requests
|
import pip_system_certs.wrapt_requests
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
|
@ -32,18 +38,9 @@ if "--syncserver" in sys.argv:
|
||||||
from anki.syncserver import run_sync_server
|
from anki.syncserver import run_sync_server
|
||||||
from anki.utils import is_mac
|
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
|
# does not return
|
||||||
run_sync_server()
|
run_sync_server()
|
||||||
|
|
||||||
from .package import packaged_build_setup
|
|
||||||
|
|
||||||
packaged_build_setup()
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import builtins
|
import builtins
|
||||||
import cProfile
|
import cProfile
|
||||||
|
@ -270,13 +267,7 @@ def setupLangAndBackend(
|
||||||
# load qt translations
|
# load qt translations
|
||||||
_qtrans = QTranslator()
|
_qtrans = QTranslator()
|
||||||
|
|
||||||
if is_mac and getattr(sys, "frozen", False):
|
qt_dir = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)
|
||||||
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("-", "_")
|
qt_lang = lang.replace("-", "_")
|
||||||
if _qtrans.load(f"qtbase_{qt_lang}", qt_dir):
|
if _qtrans.load(f"qtbase_{qt_lang}", qt_dir):
|
||||||
app.installTranslator(_qtrans)
|
app.installTranslator(_qtrans)
|
||||||
|
@ -607,14 +598,13 @@ def _run(argv: list[str] | None = None, exec: bool = True) -> AnkiApp | None:
|
||||||
profiler = cProfile.Profile()
|
profiler = cProfile.Profile()
|
||||||
profiler.enable()
|
profiler.enable()
|
||||||
|
|
||||||
packaged = getattr(sys, "frozen", False)
|
|
||||||
x11_available = os.getenv("DISPLAY")
|
x11_available = os.getenv("DISPLAY")
|
||||||
wayland_configured = qtmajor > 5 and (
|
wayland_configured = qtmajor > 5 and (
|
||||||
os.getenv("QT_QPA_PLATFORM") == "wayland" or os.getenv("WAYLAND_DISPLAY")
|
os.getenv("QT_QPA_PLATFORM") == "wayland" or os.getenv("WAYLAND_DISPLAY")
|
||||||
)
|
)
|
||||||
wayland_forced = os.getenv("ANKI_WAYLAND")
|
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:
|
if wayland_forced or not x11_available:
|
||||||
# Work around broken fractional scaling in Wayland
|
# Work around broken fractional scaling in Wayland
|
||||||
# https://bugreports.qt.io/browse/QTBUG-113574
|
# https://bugreports.qt.io/browse/QTBUG-113574
|
||||||
|
|
|
@ -14,12 +14,9 @@ import aqt.utils
|
||||||
|
|
||||||
class _MacOSHelper:
|
class _MacOSHelper:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
if getattr(sys, "frozen", False):
|
path = os.path.join(
|
||||||
path = os.path.join(sys.prefix, "libankihelper.dylib")
|
aqt.utils.aqt_data_folder(), "lib", "libankihelper.dylib"
|
||||||
else:
|
)
|
||||||
path = os.path.join(
|
|
||||||
aqt.utils.aqt_data_folder(), "lib", "libankihelper.dylib"
|
|
||||||
)
|
|
||||||
|
|
||||||
self._dll = CDLL(path)
|
self._dll = CDLL(path)
|
||||||
self._dll.system_is_dark.restype = c_bool
|
self._dll.system_is_dark.restype = c_bool
|
||||||
|
|
|
@ -252,14 +252,8 @@ def _handle_local_file_request(request: LocalFileRequest) -> Response:
|
||||||
def _builtin_data(path: str) -> bytes:
|
def _builtin_data(path: str) -> bytes:
|
||||||
"""Return data from file in aqt/data folder.
|
"""Return data from file in aqt/data folder.
|
||||||
Path must use forward slash separators."""
|
Path must use forward slash separators."""
|
||||||
# packaged build?
|
full_path = aqt_data_path() / ".." / path
|
||||||
if getattr(sys, "frozen", False):
|
return full_path.read_bytes()
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
def _handle_builtin_file_request(request: BundledFileRequest) -> Response:
|
def _handle_builtin_file_request(request: BundledFileRequest) -> Response:
|
||||||
|
|
|
@ -4,94 +4,51 @@
|
||||||
"""Helpers for the packaged version of Anki."""
|
"""Helpers for the packaged version of Anki."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
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:
|
Currently, we just import our main libraries and invoke
|
||||||
# extend sys.path with .pth files
|
mpv/lame on macOS, which is slow on the first run, and doing
|
||||||
import site
|
it this way shows progress being made.
|
||||||
|
"""
|
||||||
|
|
||||||
site.addsitedir(sys.path[0])
|
if not is_mac:
|
||||||
|
|
||||||
# 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):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
print("Initial setup...")
|
def _dot():
|
||||||
|
print(".", flush=True, end="")
|
||||||
|
|
||||||
if sys.platform == "win32":
|
_dot()
|
||||||
_fix_pywin32()
|
import anki.collection
|
||||||
elif sys.platform == "darwin":
|
_dot()
|
||||||
_fix_protobuf_path()
|
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()
|
import anki_audio
|
||||||
_patch_certifi()
|
audio_pkg_path = Path(anki_audio.__file__).parent
|
||||||
|
|
||||||
# escape hatch for debugging issues with packaged build startup
|
# Invoke mpv and lame
|
||||||
if os.getenv("ANKI_STARTUP_REPL"):
|
cmd = [Path(""), "--version"]
|
||||||
# mypy incorrectly thinks this does not exist on Windows
|
for cmd_name in ["mpv", "lame"]:
|
||||||
is_tty = os.isatty(sys.stdin.fileno()) # type: ignore
|
_dot()
|
||||||
if is_tty:
|
cmd[0] = audio_pkg_path / cmd_name
|
||||||
import code
|
subprocess.run(cmd, check=True, capture_output=True)
|
||||||
|
|
||||||
code.InteractiveConsole().interact()
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
|
@ -87,24 +87,15 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
def aqt_data_path() -> Path:
|
def aqt_data_path() -> Path:
|
||||||
# packaged?
|
import _aqt.colors
|
||||||
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")
|
data_folder = Path(inspect.getfile(_aqt.colors)).with_name("data")
|
||||||
if data_folder.exists():
|
if data_folder.exists():
|
||||||
return data_folder.absolute()
|
return data_folder.absolute()
|
||||||
else:
|
else:
|
||||||
# should only happen when running unit tests
|
# should only happen when running unit tests
|
||||||
print("warning, data folder not found")
|
print("warning, data folder not found")
|
||||||
return Path(".")
|
return Path(".")
|
||||||
|
|
||||||
|
|
||||||
def aqt_data_folder() -> str:
|
def aqt_data_folder() -> str:
|
||||||
|
@ -1207,12 +1198,11 @@ def supportText() -> str:
|
||||||
platname = platform.platform()
|
platname = platform.platform()
|
||||||
|
|
||||||
return """\
|
return """\
|
||||||
Anki {} {} {}
|
Anki {} {}
|
||||||
Python {} Qt {} PyQt {}
|
Python {} Qt {} PyQt {}
|
||||||
Platform: {}
|
Platform: {}
|
||||||
""".format(
|
""".format(
|
||||||
version_with_build(),
|
version_with_build(),
|
||||||
"(src)" if not getattr(sys, "frozen", False) else "",
|
|
||||||
"(ao)" if mw.addonManager.dirty else "",
|
"(ao)" if mw.addonManager.dirty else "",
|
||||||
platform.python_version(),
|
platform.python_version(),
|
||||||
qVersion(),
|
qVersion(),
|
||||||
|
|
|
@ -87,7 +87,10 @@ fn main() {
|
||||||
// Pre-validate by running --version to trigger any Gatekeeper checks
|
// Pre-validate by running --version to trigger any Gatekeeper checks
|
||||||
let anki_bin = uv_install_root.join(".venv/bin/anki");
|
let anki_bin = uv_install_root.join(".venv/bin/anki");
|
||||||
println!("\n\x1B[1mThis may take a few minutes. Please wait...\x1B[0m");
|
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
|
// Then launch the binary as detached subprocess so the terminal can close
|
||||||
let child = Command::new(&anki_bin)
|
let child = Command::new(&anki_bin)
|
||||||
|
@ -98,7 +101,6 @@ fn main() {
|
||||||
.spawn()
|
.spawn()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
std::mem::forget(child);
|
std::mem::forget(child);
|
||||||
println!("Anki launched successfully");
|
|
||||||
} else {
|
} else {
|
||||||
// If venv already existed, exec as normal
|
// If venv already existed, exec as normal
|
||||||
println!(
|
println!(
|
||||||
|
|
Loading…
Reference in a new issue