Split libankihelper into a separate module

It's rarely updated, and the old approach resulted in a 'proper' aqt
build only being done on a Mac.
This commit is contained in:
Damien Elmes 2025-06-20 16:13:49 +07:00
parent 344cac1ef4
commit cd411927cc
9 changed files with 125 additions and 71 deletions

View file

@ -27,7 +27,6 @@ pub fn build_and_check_aqt(build: &mut Build) -> Result<()> {
build_forms(build)?; build_forms(build)?;
build_generated_sources(build)?; build_generated_sources(build)?;
build_data_folder(build)?; build_data_folder(build)?;
build_macos_helper(build)?;
build_wheel(build)?; build_wheel(build)?;
check_python(build)?; check_python(build)?;
Ok(()) 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<()> { fn build_wheel(build: &mut Build) -> Result<()> {
build.add_action( build.add_action(
"wheels:aqt", "wheels:aqt",

View file

@ -3,50 +3,11 @@
from __future__ import annotations from __future__ import annotations
import os
import sys 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": if sys.platform == "darwin":
try: from anki_mac_helper import ( # pylint:disable=unused-import,import-error
macos_helper = _MacOSHelper() macos_helper,
except Exception as e: )
print("macos_helper:", e) else:
macos_helper = None

View file

@ -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)

View file

20
qt/mac/build.sh Executable file
View file

@ -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!"

View file

@ -7,8 +7,16 @@ import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
out_dylib, *src_files = sys.argv[1:] # If no arguments provided, build for the anki_mac_helper package
out_dir = Path(out_dylib).parent.resolve() 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() src_dir = Path(src_files[0]).parent.resolve()
# Build for both architectures # Build for both architectures
@ -29,12 +37,20 @@ for arch in architectures:
"ankihelper", "ankihelper",
"-O", "-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)]) args.extend(["-o", str(temp_out)])
subprocess.run(args, check=True, cwd=out_dir) subprocess.run(args, check=True, cwd=out_dir)
# Ensure output directory exists
out_dir.mkdir(parents=True, exist_ok=True)
# Create universal binary # 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) subprocess.run(lipo_args, check=True)
# Clean up temporary files # Clean up temporary files

17
qt/mac/pyproject.toml Normal file
View file

@ -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"]

View file

@ -14,6 +14,7 @@ dependencies = [
"waitress>=2.0.0", "waitress>=2.0.0",
"psutil; sys.platform == 'win32'", "psutil; sys.platform == 'win32'",
"pywin32; sys.platform == 'win32'", "pywin32; sys.platform == 'win32'",
"anki-mac-helper; sys.platform == 'darwin'",
"pip-system-certs!=5.1", "pip-system-certs!=5.1",
"mock", "mock",
"types-decorator", "types-decorator",

12
uv.lock
View file

@ -141,11 +141,20 @@ dev = [
{ name = "wheel" }, { 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]] [[package]]
name = "aqt" name = "aqt"
version = "0.1.2" version = "0.1.2"
source = { editable = "qt" } source = { editable = "qt" }
dependencies = [ 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 = "beautifulsoup4" },
{ name = "flask" }, { name = "flask" },
{ name = "flask-cors" }, { name = "flask-cors" },
@ -201,6 +210,7 @@ qt67 = [
[package.metadata] [package.metadata]
requires-dist = [ 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-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 = "beautifulsoup4" },
{ name = "flask" }, { name = "flask" },
{ name = "flask-cors" }, { name = "flask-cors" },
@ -572,7 +582,7 @@ name = "importlib-metadata"
version = "8.7.0" version = "8.7.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ 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" } 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 = [ wheels = [