feat: add compatibility layer for LXST vendored pyogg to handle ctypes aliases, improve installation process and ensure proper imports

This commit is contained in:
Ivan 2026-07-22 16:14:41 -05:00
parent b27028a7fb
commit f4d561cc86
No known key found for this signature in database
9 changed files with 377 additions and 79 deletions

View file

@ -67,6 +67,7 @@ COPY vendor ./vendor
COPY scripts/docker-bake-lxst-filterlib-musl.py ./scripts/docker-bake-lxst-filterlib-musl.py
COPY scripts/patch_lxst_pyogg_ogg_ctypes.py ./scripts/patch_lxst_pyogg_ogg_ctypes.py
COPY scripts/patch_lxst_codec2_optional.py ./scripts/patch_lxst_codec2_optional.py
COPY meshchatx/src/backend/lxst_pyogg_ctypes_compat.py ./meshchatx/src/backend/lxst_pyogg_ctypes_compat.py
# Third-party deps layer: stable across app-only updates when uv.lock is unchanged.
# --inexact keeps setuptools/jaraco.context already in the venv (cffi bake needs them).
RUN uv sync --no-group dev --no-install-project --inexact && \

View file

@ -58,6 +58,7 @@ COPY logo ./logo
COPY vendor ./vendor
COPY scripts/patch_lxst_pyogg_ogg_ctypes.py ./scripts/patch_lxst_pyogg_ogg_ctypes.py
COPY scripts/patch_lxst_codec2_optional.py ./scripts/patch_lxst_codec2_optional.py
COPY meshchatx/src/backend/lxst_pyogg_ctypes_compat.py ./meshchatx/src/backend/lxst_pyogg_ctypes_compat.py
# Third-party deps layer: stable across app-only updates when uv.lock is unchanged.
# --inexact keeps setuptools/jaraco.context already in the venv (cffi needs them).
RUN uv sync --no-group dev --no-install-project --inexact && \

View file

@ -72,6 +72,8 @@ meshchatx --headless --host 127.0.0.1
The `meshchat` command is a compatibility alias for the same entry point.
On hosts where `libopus` is installed but `libogg` is not, LXST's vendored pyogg can raise `NameError: c_int_p` on import. MeshChatX applies a ctypes compatibility fix at startup (the same patch Docker runs after install). Optional telephony audio still needs the usual Opus/Ogg system libraries when you use those codecs.
## Linux AppImage and packages
**AppImage**

Binary file not shown.

View file

@ -4,3 +4,14 @@
# Synced from package.json via scripts/sync_version.js (also writes meshchatx/src/version.py).
__version__ = "4.8.0"
# LXST vendored pyogg can NameError on import when libopus is present but
# libogg is not. Apply before any meshchatx module imports LXST.
try:
from meshchatx.src.backend.lxst_pyogg_ctypes_compat import (
ensure_lxst_pyogg_ctypes_compat,
)
ensure_lxst_pyogg_ctypes_compat()
except (ImportError, OSError, ValueError):
pass

View file

@ -42,9 +42,12 @@ from urllib.parse import urlparse
import aiohttp
import bcrypt
import LXMF
import LXST
import psutil
import RNS
# meshchatx/__init__ already ensures pyogg ctypes aliases. Import LXST after
# that package init so plain pip installs do not crash without the Docker patch.
import LXST
from aiohttp import WSCloseCode, WSMessage, WSMsgType, web
from aiohttp_session import get_session
from aiohttp_session import setup as setup_session

View file

@ -0,0 +1,204 @@
# SPDX-License-Identifier: 0BSD
"""Ensure LXST vendored pyogg exports ctypes aliases Opus needs.
Upstream ogg.py defines c_int_p and related names only inside
if PYOGG_OGG_AVAIL. When libopus loads but libogg does not, opus.py
still references those names and raises NameError on import.
Docker and Taskfile run a post-install disk patch. Plain pip install
does not, so MeshChatX applies the same fix before importing LXST.
"""
from __future__ import annotations
import importlib.abc
import importlib.metadata
import importlib.util
import sys
from ctypes import POINTER, c_char_p, c_float, c_int, c_ubyte
from pathlib import Path
_MARKER = "# meshchatx-lxst-pyogg-ctypes-compat (do not remove; idempotency)\n"
_LEGACY_MARKER = (
"# meshchatx-lxst-pyogg-ctypes-pointer-aliases (do not remove; idempotency)\n"
)
_LEGACY_BLOCK = (
_LEGACY_MARKER
+ "c_int_p = POINTER(c_int)\n"
+ "c_float_p = POINTER(c_float)\n"
+ "c_uchar_p = POINTER(c_ubyte)\n"
+ "c_char_p_p = POINTER(c_char_p)\n"
)
_INSERT = (
_MARKER
+ "c_int_p = POINTER(c_int)\n"
+ "c_float_p = POINTER(c_float)\n"
+ "c_uchar_p = POINTER(c_ubyte)\n"
+ "c_char_p_p = POINTER(c_char_p)\n"
+ "c_uchar = c_ubyte\n"
)
_NEEDLE = (
"from ctypes import c_int, c_int8, c_int16, c_int32, c_int64, c_uint, c_uint8, "
"c_uint16, c_uint32, c_uint64, c_float, c_long, c_ulong, c_char, c_char_p, "
"c_ubyte, c_longlong, c_ulonglong, c_size_t, c_void_p, c_double, POINTER, "
"pointer, cast\n"
)
_OGG_MODULE = "LXST.Codecs.libs.pyogg.ogg"
_HOOK_INSTALLED = False
_ALIAS_VALUES = {
"c_int_p": POINTER(c_int),
"c_float_p": POINTER(c_float),
"c_uchar_p": POINTER(c_ubyte),
"c_char_p_p": POINTER(c_char_p),
"c_uchar": c_ubyte,
}
def find_ogg_py() -> Path | None:
"""Locate LXST bundled pyogg ogg.py without importing LXST."""
try:
dist = importlib.metadata.distribution("lxst")
except importlib.metadata.PackageNotFoundError:
return None
for rel in dist.files or ():
parts = rel.parts
if len(parts) >= 2 and parts[-2] == "pyogg" and parts[-1] == "ogg.py":
return Path(dist.locate_file(rel))
return None
def inject_ctypes_aliases(ogg_module) -> list[str]:
"""Set missing ctypes aliases on a loaded ogg module. Returns names set."""
added: list[str] = []
for name, value in _ALIAS_VALUES.items():
if not hasattr(ogg_module, name):
setattr(ogg_module, name, value)
added.append(name)
return added
def apply_disk_patch(ogg: Path | None = None) -> str:
"""Patch ogg.py on disk. Returns status token."""
target = ogg if ogg is not None else find_ogg_py()
if target is None:
return "missing"
text = target.read_text(encoding="utf-8")
if _MARKER in text:
return "already"
if _LEGACY_BLOCK in text:
target.write_text(text.replace(_LEGACY_BLOCK, _INSERT, 1), encoding="utf-8")
return "upgraded"
if _NEEDLE not in text:
return "unexpected"
target.write_text(text.replace(_NEEDLE, _NEEDLE + _INSERT, 1), encoding="utf-8")
return "patched"
class _OggCompatLoader(importlib.abc.Loader):
"""Load ogg.py then inject module-level ctypes aliases."""
def __init__(self, origin: Path):
self.origin = origin
def create_module(self, spec):
return None
def exec_module(self, module):
source = self.origin.read_text(encoding="utf-8")
code = compile(source, str(self.origin), "exec")
exec(code, module.__dict__)
inject_ctypes_aliases(module)
class _OggCompatFinder(importlib.abc.MetaPathFinder):
"""Intercept LXST pyogg ogg import when disk patch is unavailable."""
def find_spec(self, fullname, path, target=None):
if fullname != _OGG_MODULE:
return None
if fullname in sys.modules:
return None
ogg = find_ogg_py()
if ogg is None or not ogg.is_file():
return None
try:
text = ogg.read_text(encoding="utf-8")
except OSError:
return None
if _MARKER in text:
return None
loader = _OggCompatLoader(ogg)
return importlib.util.spec_from_file_location(
fullname,
ogg,
loader=loader,
submodule_search_locations=None,
)
def install_import_hook() -> bool:
"""Install a one-shot meta path finder for read-only site-packages."""
global _HOOK_INSTALLED
if _HOOK_INSTALLED:
return False
for finder in sys.meta_path:
if isinstance(finder, _OggCompatFinder):
_HOOK_INSTALLED = True
return False
sys.meta_path.insert(0, _OggCompatFinder())
_HOOK_INSTALLED = True
return True
def ensure_lxst_pyogg_ctypes_compat() -> str:
"""Disk-patch when possible and install an import hook as fallback.
Safe to call repeatedly. Does not import LXST.
"""
status = "skipped"
try:
status = apply_disk_patch()
except OSError:
status = "readonly"
install_import_hook()
existing = sys.modules.get(_OGG_MODULE)
if existing is not None:
inject_ctypes_aliases(existing)
return status
def patch_cli() -> int:
"""CLI used by scripts/patch_lxst_pyogg_ogg_ctypes.py."""
try:
status = apply_disk_patch()
except OSError as exc:
print(f"patch_lxst_pyogg_ogg_ctypes: write failed ({exc})", file=sys.stderr)
return 1
ogg = find_ogg_py()
if status == "missing":
print(
"patch_lxst_pyogg_ogg_ctypes: lxst not installed, nothing to do",
file=sys.stderr,
)
return 0
if status == "already":
print(f"patch_lxst_pyogg_ogg_ctypes: already applied ({ogg})")
return 0
if status == "upgraded":
print(f"patch_lxst_pyogg_ogg_ctypes: upgraded legacy block ({ogg})")
return 0
if status == "unexpected":
print(
"patch_lxst_pyogg_ogg_ctypes: unexpected ogg.py layout; "
f"manual check required ({ogg})",
file=sys.stderr,
)
return 1
print(f"patch_lxst_pyogg_ogg_ctypes: patched {ogg}")
return 0

View file

@ -1,95 +1,46 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: 0BSD
"""Patch LXST bundled pyogg ogg.py for Python 3.14+.
"""Patch LXST bundled pyogg ogg.py for missing ctypes aliases.
opus.py does from .ogg import * but needs extra ctypes names that
ogg.py never defined: POINTER aliases, and c_uchar (used as c_uchar*0
for flexible array argtypes; same layout as c_ubyte).
ogg.py only defines when libogg loads. Idempotent: safe after every
pip install / poetry install / uv sync.
Idempotent: safe to run after every pip install / poetry install.
MeshChatX also applies this at package import for plain PyPI installs.
This script remains for Docker, CI, and packaging (including the Docker
deps layer before the project package is installed).
"""
from __future__ import annotations
import importlib.metadata
import sys
import importlib.util
from pathlib import Path
_MARKER = "# meshchatx-lxst-pyogg-ctypes-compat (do not remove; idempotency)\n"
# First revision (older Docker/local installs): bump handled below.
_LEGACY_MARKER = (
"# meshchatx-lxst-pyogg-ctypes-pointer-aliases (do not remove; idempotency)\n"
)
_LEGACY_BLOCK = (
_LEGACY_MARKER
+ "c_int_p = POINTER(c_int)\n"
+ "c_float_p = POINTER(c_float)\n"
+ "c_uchar_p = POINTER(c_ubyte)\n"
+ "c_char_p_p = POINTER(c_char_p)\n"
)
_INSERT = (
_MARKER
+ "c_int_p = POINTER(c_int)\n"
+ "c_float_p = POINTER(c_float)\n"
+ "c_uchar_p = POINTER(c_ubyte)\n"
+ "c_char_p_p = POINTER(c_char_p)\n"
+ "c_uchar = c_ubyte\n"
)
_NEEDLE = (
"from ctypes import c_int, c_int8, c_int16, c_int32, c_int64, c_uint, c_uint8, "
"c_uint16, c_uint32, c_uint64, c_float, c_long, c_ulong, c_char, c_char_p, "
"c_ubyte, c_longlong, c_ulonglong, c_size_t, c_void_p, c_double, POINTER, "
"pointer, cast\n"
)
def _find_ogg_py() -> Path | None:
def _load_compat():
try:
dist = importlib.metadata.distribution("lxst")
except importlib.metadata.PackageNotFoundError:
return None
for rel in dist.files or ():
parts = rel.parts
if len(parts) >= 2 and parts[-2] == "pyogg" and parts[-1] == "ogg.py":
return Path(dist.locate_file(rel))
return None
from meshchatx.src.backend.lxst_pyogg_ctypes_compat import patch_cli
def main() -> int:
ogg = _find_ogg_py()
if ogg is None:
print(
"patch_lxst_pyogg_ogg_ctypes: lxst not installed, nothing to do",
file=sys.stderr,
)
return 0
text = ogg.read_text(encoding="utf-8")
if _MARKER in text:
print(f"patch_lxst_pyogg_ogg_ctypes: already applied ({ogg})")
return 0
if _LEGACY_BLOCK in text:
ogg.write_text(text.replace(_LEGACY_BLOCK, _INSERT, 1), encoding="utf-8")
print(f"patch_lxst_pyogg_ogg_ctypes: upgraded legacy block ({ogg})")
return 0
if _NEEDLE not in text:
print(
"patch_lxst_pyogg_ogg_ctypes: unexpected ogg.py layout; "
f"manual check required ({ogg})",
file=sys.stderr,
)
return 1
ogg.write_text(text.replace(_NEEDLE, _NEEDLE + _INSERT, 1), encoding="utf-8")
print(f"patch_lxst_pyogg_ogg_ctypes: patched {ogg}")
return 0
return patch_cli
except ImportError:
pass
path = (
Path(__file__).resolve().parents[1]
/ "meshchatx"
/ "src"
/ "backend"
/ "lxst_pyogg_ctypes_compat.py"
)
spec = importlib.util.spec_from_file_location(
"lxst_pyogg_ctypes_compat",
path,
)
if spec is None or spec.loader is None:
raise ImportError(f"cannot load compat module from {path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module.patch_cli
if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(_load_compat()())

View file

@ -0,0 +1,125 @@
# SPDX-License-Identifier: 0BSD
"""Tests for LXST pyogg ctypes alias compatibility."""
from __future__ import annotations
import sys
import textwrap
from pathlib import Path
import pytest
from meshchatx.src.backend import lxst_pyogg_ctypes_compat as compat
_NEEDLE = compat._NEEDLE
def _ogg_source(*, with_marker: bool = False, with_legacy: bool = False) -> str:
body = _NEEDLE
if with_marker:
body += compat._INSERT
elif with_legacy:
body += compat._LEGACY_BLOCK
body += textwrap.dedent(
"""
libogg = None
PYOGG_OGG_AVAIL = False
"""
)
return body
def test_apply_disk_patch_inserts_module_level_aliases(tmp_path: Path):
ogg = tmp_path / "ogg.py"
ogg.write_text(_ogg_source(), encoding="utf-8")
assert compat.apply_disk_patch(ogg) == "patched"
text = ogg.read_text(encoding="utf-8")
assert compat._MARKER in text
assert "c_int_p = POINTER(c_int)" in text
assert "c_uchar = c_ubyte" in text
assert compat.apply_disk_patch(ogg) == "already"
def test_apply_disk_patch_upgrades_legacy_block(tmp_path: Path):
ogg = tmp_path / "ogg.py"
ogg.write_text(_ogg_source(with_legacy=True), encoding="utf-8")
assert compat.apply_disk_patch(ogg) == "upgraded"
text = ogg.read_text(encoding="utf-8")
assert compat._MARKER in text
assert compat._LEGACY_MARKER not in text
assert "c_uchar = c_ubyte" in text
def test_apply_disk_patch_unexpected_layout(tmp_path: Path):
ogg = tmp_path / "ogg.py"
ogg.write_text("print('not pyogg')\n", encoding="utf-8")
assert compat.apply_disk_patch(ogg) == "unexpected"
def test_inject_ctypes_aliases_only_fills_missing():
module = type(sys)("fake_ogg")
module.c_int_p = object()
added = compat.inject_ctypes_aliases(module)
assert "c_int_p" not in added
assert "c_float_p" in added
assert "c_uchar" in added
assert hasattr(module, "c_float_p")
assert hasattr(module, "c_uchar")
def test_import_hook_loader_injects_aliases(tmp_path: Path):
ogg = tmp_path / "ogg.py"
ogg.write_text(_ogg_source(), encoding="utf-8")
module = type(sys)("test_ogg_compat")
module.__file__ = str(ogg)
compat._OggCompatLoader(ogg).exec_module(module)
assert hasattr(module, "c_int_p")
assert hasattr(module, "c_uchar")
assert module.PYOGG_OGG_AVAIL is False
def test_finder_returns_spec_only_when_unpatched(tmp_path: Path, monkeypatch):
ogg = tmp_path / "ogg.py"
ogg.write_text(_ogg_source(), encoding="utf-8")
monkeypatch.setattr(compat, "find_ogg_py", lambda: ogg)
sys.modules.pop(compat._OGG_MODULE, None)
finder = compat._OggCompatFinder()
spec = finder.find_spec(compat._OGG_MODULE, None)
assert spec is not None
assert isinstance(spec.loader, compat._OggCompatLoader)
ogg.write_text(_ogg_source(with_marker=True), encoding="utf-8")
assert finder.find_spec(compat._OGG_MODULE, None) is None
def test_ensure_prefers_disk_patch(tmp_path: Path, monkeypatch):
ogg = tmp_path / "ogg.py"
ogg.write_text(_ogg_source(), encoding="utf-8")
monkeypatch.setattr(compat, "find_ogg_py", lambda: ogg)
compat._HOOK_INSTALLED = False
sys.meta_path[:] = [
f for f in sys.meta_path if not isinstance(f, compat._OggCompatFinder)
]
status = compat.ensure_lxst_pyogg_ctypes_compat()
assert status == "patched"
assert compat._MARKER in ogg.read_text(encoding="utf-8")
@pytest.mark.parametrize(
("status", "expected_code"),
[
("missing", 0),
("already", 0),
("upgraded", 0),
("patched", 0),
("unexpected", 1),
],
)
def test_patch_cli_exit_codes(monkeypatch, status, expected_code):
monkeypatch.setattr(compat, "apply_disk_patch", lambda: status)
monkeypatch.setattr(compat, "find_ogg_py", lambda: Path("/tmp/ogg.py"))
assert compat.patch_cli() == expected_code