diff --git a/build/configure/src/aqt.rs b/build/configure/src/aqt.rs index 60316fcd2..6be5adb07 100644 --- a/build/configure/src/aqt.rs +++ b/build/configure/src/aqt.rs @@ -27,7 +27,6 @@ pub fn build_and_check_aqt(build: &mut Build) -> Result<()> { build_forms(build)?; build_generated_sources(build)?; build_data_folder(build)?; - build_macos_helper(build)?; build_wheel(build)?; check_python(build)?; Ok(()) @@ -337,27 +336,6 @@ impl BuildAction for BuildThemedIcon<'_> { } } -fn build_macos_helper(build: &mut Build) -> Result<()> { - if cfg!(target_os = "macos") { - build.add_action( - "qt:aqt:data:lib:libankihelper", - RunCommand { - command: ":pyenv:bin", - args: "$script $out $in", - inputs: hashmap! { - "script" => inputs!["qt/mac/helper_build.py"], - "in" => inputs![glob!["qt/mac/*.swift"]], - "" => inputs!["out/env"], - }, - outputs: hashmap! { - "out" => vec!["qt/_aqt/data/lib/libankihelper.dylib"], - }, - }, - )?; - } - Ok(()) -} - fn build_wheel(build: &mut Build) -> Result<()> { build.add_action( "wheels:aqt", diff --git a/qt/aqt/_macos_helper.py b/qt/aqt/_macos_helper.py index 9328b1c4a..634d94756 100644 --- a/qt/aqt/_macos_helper.py +++ b/qt/aqt/_macos_helper.py @@ -3,50 +3,11 @@ from __future__ import annotations -import os import sys -from collections.abc import Callable -from ctypes import CDLL, CFUNCTYPE, c_bool, c_char_p -import aqt -import aqt.utils - - -class _MacOSHelper: - def __init__(self) -> None: - path = os.path.join(aqt.utils.aqt_data_folder(), "lib", "libankihelper.dylib") - - self._dll = CDLL(path) - self._dll.system_is_dark.restype = c_bool - - def system_is_dark(self) -> bool: - return self._dll.system_is_dark() - - def set_darkmode_enabled(self, enabled: bool) -> bool: - return self._dll.set_darkmode_enabled(enabled) - - def start_wav_record(self, path: str, on_error: Callable[[str], None]) -> None: - global _on_audio_error - _on_audio_error = on_error - self._dll.start_wav_record(path.encode("utf8"), _audio_error_callback) - - def end_wav_record(self) -> None: - "On completion, file should be saved if no error has arrived." - self._dll.end_wav_record() - - -# this must not be overwritten or deallocated -@CFUNCTYPE(None, c_char_p) # type: ignore -def _audio_error_callback(msg: str) -> None: - if handler := _on_audio_error: - handler(msg) - - -_on_audio_error: Callable[[str], None] | None = None - -macos_helper: _MacOSHelper | None = None if sys.platform == "darwin": - try: - macos_helper = _MacOSHelper() - except Exception as e: - print("macos_helper:", e) + from anki_mac_helper import ( # pylint:disable=unused-import,import-error + macos_helper, + ) +else: + macos_helper = None diff --git a/qt/mac/anki_mac_helper/__init__.py b/qt/mac/anki_mac_helper/__init__.py new file mode 100644 index 000000000..a0adb469e --- /dev/null +++ b/qt/mac/anki_mac_helper/__init__.py @@ -0,0 +1,51 @@ +# 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 sys +from collections.abc import Callable +from ctypes import CDLL, CFUNCTYPE, c_bool, c_char_p +from pathlib import Path + + +class _MacOSHelper: + def __init__(self) -> None: + # Look for the dylib in the same directory as this module + module_dir = Path(__file__).parent + path = module_dir / "libankihelper.dylib" + + self._dll = CDLL(str(path)) + self._dll.system_is_dark.restype = c_bool + + def system_is_dark(self) -> bool: + return self._dll.system_is_dark() + + def set_darkmode_enabled(self, enabled: bool) -> bool: + return self._dll.set_darkmode_enabled(enabled) + + def start_wav_record(self, path: str, on_error: Callable[[str], None]) -> None: + global _on_audio_error + _on_audio_error = on_error + self._dll.start_wav_record(path.encode("utf8"), _audio_error_callback) + + def end_wav_record(self) -> None: + "On completion, file should be saved if no error has arrived." + self._dll.end_wav_record() + + +# this must not be overwritten or deallocated +@CFUNCTYPE(None, c_char_p) # type: ignore +def _audio_error_callback(msg: str) -> None: + if handler := _on_audio_error: + handler(msg) + + +_on_audio_error: Callable[[str], None] | None = None + +macos_helper: _MacOSHelper | None = None +if sys.platform == "darwin": + try: + macos_helper = _MacOSHelper() + except Exception as e: + print("macos_helper:", e) diff --git a/qt/mac/anki_mac_helper/py.typed b/qt/mac/anki_mac_helper/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/qt/mac/build.sh b/qt/mac/build.sh new file mode 100755 index 000000000..4c14a13f4 --- /dev/null +++ b/qt/mac/build.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +set -e + +# Get the project root directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJ_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# Build the dylib first +echo "Building macOS helper dylib..." +"$PROJ_ROOT/out/pyenv/bin/python" "$SCRIPT_DIR/helper_build.py" + +# Create the wheel using uv +echo "Creating wheel..." +cd "$SCRIPT_DIR" +"$PROJ_ROOT/out/extracted/uv/uv" build --wheel + +echo "Build complete!" diff --git a/qt/mac/helper_build.py b/qt/mac/helper_build.py index 0638c9249..aaf997669 100644 --- a/qt/mac/helper_build.py +++ b/qt/mac/helper_build.py @@ -7,8 +7,16 @@ import subprocess import sys from pathlib import Path -out_dylib, *src_files = sys.argv[1:] -out_dir = Path(out_dylib).parent.resolve() +# If no arguments provided, build for the anki_mac_helper package +if len(sys.argv) == 1: + script_dir = Path(__file__).parent + out_dylib = script_dir / "anki_mac_helper" / "libankihelper.dylib" + src_files = list(script_dir.glob("*.swift")) +else: + out_dylib, *src_files = sys.argv[1:] + +out_dylib = Path(out_dylib) +out_dir = out_dylib.parent.resolve() src_dir = Path(src_files[0]).parent.resolve() # Build for both architectures @@ -29,12 +37,20 @@ for arch in architectures: "ankihelper", "-O", ] - args.extend(src_dir / Path(file).name for file in src_files) + if isinstance(src_files[0], Path): + args.extend(src_files) + else: + args.extend(src_dir / Path(file).name for file in src_files) args.extend(["-o", str(temp_out)]) subprocess.run(args, check=True, cwd=out_dir) +# Ensure output directory exists +out_dir.mkdir(parents=True, exist_ok=True) + # Create universal binary -lipo_args = ["lipo", "-create", "-output", out_dylib] + [str(f) for f in temp_files] +lipo_args = ["lipo", "-create", "-output", str(out_dylib)] + [ + str(f) for f in temp_files +] subprocess.run(lipo_args, check=True) # Clean up temporary files diff --git a/qt/mac/pyproject.toml b/qt/mac/pyproject.toml new file mode 100644 index 000000000..93f4e939b --- /dev/null +++ b/qt/mac/pyproject.toml @@ -0,0 +1,17 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "anki-mac-helper" +version = "0.1.0" +description = "Small support library for Anki on Macs" +requires-python = ">=3.9" +license = { text = "AGPL-3.0-or-later" } +authors = [ + { name = "Anki Team" }, +] +urls = { Homepage = "https://github.com/ankitects/anki" } + +[tool.hatch.build.targets.wheel] +packages = ["anki_mac_helper"] diff --git a/qt/pyproject.toml b/qt/pyproject.toml index ab16afc3d..d8b4c4306 100644 --- a/qt/pyproject.toml +++ b/qt/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ "waitress>=2.0.0", "psutil; sys.platform == 'win32'", "pywin32; sys.platform == 'win32'", + "anki-mac-helper; sys.platform == 'darwin'", "pip-system-certs!=5.1", "mock", "types-decorator", diff --git a/uv.lock b/uv.lock index d735e0772..51b16f8ab 100644 --- a/uv.lock +++ b/uv.lock @@ -141,11 +141,20 @@ dev = [ { name = "wheel" }, ] +[[package]] +name = "anki-mac-helper" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/9f/c4d3e635ddbd2c6c24ff5454e96900fd2061b9abbb0198b9283446780d08/anki_mac_helper-0.1.0-py3-none-any.whl", hash = "sha256:ed449aba27ea3bc7999054afa10dacf08ef856ed7af46526d9c8599d8179a618", size = 40637, upload-time = "2025-06-19T14:38:07.672Z" }, +] + [[package]] name = "aqt" version = "0.1.2" source = { editable = "qt" } dependencies = [ + { name = "anki-mac-helper", marker = "sys_platform == 'darwin' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, { name = "beautifulsoup4" }, { name = "flask" }, { name = "flask-cors" }, @@ -201,6 +210,7 @@ qt67 = [ [package.metadata] requires-dist = [ { name = "anki-audio", marker = "(sys_platform == 'darwin' and extra == 'audio') or (sys_platform == 'win32' and extra == 'audio')", specifier = "==0.1.0" }, + { name = "anki-mac-helper", marker = "sys_platform == 'darwin'" }, { name = "beautifulsoup4" }, { name = "flask" }, { name = "flask-cors" }, @@ -572,7 +582,7 @@ name = "importlib-metadata" version = "8.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp" }, + { name = "zipp", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } wheels = [