From f8a604fc7c99dbea18606244ed6a0f62a16ae98d Mon Sep 17 00:00:00 2001 From: Viktor Petersson Date: Fri, 10 Jul 2026 19:53:56 +0000 Subject: [PATCH 1/8] fix(webview): stop Pi 2/3 blank screen from gcc NEON alignment SIGBUS The Qt5 WebEngine viewer crash-loops on the Pi 2 (and any armv7/Cortex-A board), leaving a permanent blank screen: AnthiasViewer SIGBUSes on an unaligned 64-bit NEON store (vst1 {q8},[rN:64]) that modern Debian gcc (12-16) emits for struct zero-init/copy of 8-byte-aligned types via its arm_block_set_aligned_vect block-move expander. Chromium/Qt hand some of those pointers 4-byte-aligned addresses, so the :64 alignment assertion faults on the Cortex-A7; the kernel can't fix up NEON, so it SIGBUSes. gcc treats this as correct codegen (GCC PR 93031, WONTFIX); the previously shipped toolchain only escaped it because it predated the Linaro->Debian gcc switch. Fix (both required): - Append arm_use_neon=false to QtWebEngine's Chromium gn args (the fix Yocto's meta-chromium / meta-lgsvl-browser ship) so general C++ compiles -mfpu=vfpv3-d16; codec/graphics (libvpx, Skia) re-add NEON per-file with runtime detection, preserving HW SIMD decode. - Wrap the armhf cross gcc/g++ with -mno-unaligned-access so BOTH the qmake-built Qt libs and Chromium's gn build lower the block move to 4-byte-safe vstr. arm_use_neon=false alone doesn't cover Qt's own libs. Also makes the Qt5 toolchain rebuildable on Debian trixie (Python 3.13), which dropped stdlib modules Chromium 87's build tooling still imports: imp/pipes/cgi shims (src/anthias_webview/pyshim/) installed into python3 site-packages, a distro-six overwrite for grit's vendored six, and python3-six/setuptools installs. Validated on a real Pi 2: 0 alignment traps, stable viewer, renders image/video/webpage, on the latest Qt 5.15.19 + Debian gcc-14. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/anthias_webview/build_qt5.sh | 92 +++++++++++++++++++ src/anthias_webview/pyshim/cgi.py | 11 +++ src/anthias_webview/pyshim/imp.py | 138 ++++++++++++++++++++++++++++ src/anthias_webview/pyshim/pipes.py | 6 ++ tools/image_builder/utils.py | 2 +- 5 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 src/anthias_webview/pyshim/cgi.py create mode 100644 src/anthias_webview/pyshim/imp.py create mode 100644 src/anthias_webview/pyshim/pipes.py diff --git a/src/anthias_webview/build_qt5.sh b/src/anthias_webview/build_qt5.sh index 5aa6f9294..ab4c222bb 100755 --- a/src/anthias_webview/build_qt5.sh +++ b/src/anthias_webview/build_qt5.sh @@ -18,6 +18,62 @@ QT_MINOR="15" QT_BUG_FIX="19" QT_VERSION="$QT_MAJOR.$QT_MINOR.$QT_BUG_FIX" DEBIAN_VERSION=$(lsb_release -cs) + +# Debian trixie ships Python 3.13, which removed stdlib modules Chromium +# 87's build tooling still imports: `imp` (gone 3.12), `pipes`/`cgi` +# (gone 3.13). Install importlib-backed shims (src/anthias_webview/pyshim/) +# into python3's site-packages ONLY. Do NOT put them on a shared +# PYTHONPATH: the build runs some codegen actions under python2.7 (which +# has real pipes/imp/cgi), and a shared PYTHONPATH would shadow those with +# the py3-only shim and break it ("cannot import name quote"). python3's +# site-packages is only consulted after the stdlib, so it fills the gaps +# for removed modules without shadowing anything that still exists. +if [ -d /webview/pyshim ]; then + PY3_SITE="$(python3 -c 'import sysconfig; print(sysconfig.get_path("purelib"))' 2>/dev/null || echo /usr/lib/python3/dist-packages)" + cp -f /webview/pyshim/*.py "$PY3_SITE/" 2>/dev/null || true +fi + +# Chromium 87's grit/build tooling `import six` (and six.moves); trixie's +# python3.13 ships no six by default, so the gn/ninja codegen actions +# abort with "No module named 'six.moves'". Install the distro package +# (six 1.17 supports 3.13) once at the start of the build. +if ! python3 -c 'import six.moves' 2>/dev/null; then + apt-get update -qq && apt-get install -y -qq python3-six \ + || pip3 install --break-system-packages six || true +fi +# Chromium 87's licenses/gyp tooling imports distutils (spawn, version, +# dir_util, archive_util), removed from the stdlib in Python 3.12. +# setuptools re-provides it via its distutils-precedence .pth, so just +# ensure setuptools is installed. +if ! python3 -c 'import distutils.spawn' 2>/dev/null; then + apt-get update -qq && apt-get install -y -qq python3-setuptools \ + || pip3 install --break-system-packages setuptools || true +fi + +# Force -mno-unaligned-access on EVERY armhf compile. Modern Debian gcc +# lowers struct zero-init/copy of an 8-byte-aligned type into a NEON +# block-move store with a :64 alignment assertion (arm_block_set_aligned_vect); +# on the Cortex-A7 (Pi 2) that SIGBUSes whenever the object is only +# 4-byte-aligned (Chromium/Qt misaligned-pointer UB), blanking the viewer. +# GCC won't change this (PR 93031 WONTFIX). -mno-unaligned-access makes +# arm_block_set_vect bail (`... && !unaligned_access) return false`), so gcc +# emits 4-byte-safe `vstr` instead. arm_use_neon=false (common.pri, below) +# covers only Chromium's gn C++; Qt's own libs (libQt5Core/Gui/Qml/Quick, +# built by qmake with -mfpu=neon-vfpv4) and the WebEngine integration layer +# still emit it, so wrap the compiler itself — both qmake and gn resolve +# arm-linux-gnueabihf-{gcc,g++} through /usr/bin. Resolve the real binary +# BEFORE replacing the symlink so the wrapper doesn't recurse. +for _t in gcc g++; do + _real="$(readlink -f "/usr/bin/arm-linux-gnueabihf-$_t")" + rm -f "/usr/bin/arm-linux-gnueabihf-$_t" + cat > "/usr/bin/arm-linux-gnueabihf-$_t" </dev/null || true + find "$CHROMIUM_DIR" -path '*/six/__init__.py' -print \ + -exec cp "$SYS_SIX" {} \; 2>/dev/null || true + fi + + # Force arm_use_neon=false for the Chromium build (Yocto's fix: + # OSSystems meta-chromium / lgsvl meta-lgsvl-browser). QtWebEngine + # leaves arm_use_neon unset, so Chromium's arm.gni default resolves + # it to true on Linux and applies -mfpu=neon to EVERY C++ TU. Modern + # Debian gcc (12-16) then compiles struct zero-init/copy of 8-byte- + # aligned types into a NEON block-move store with a :64 alignment + # assertion (arm_block_set_aligned_vect); on the Cortex-A7 (Pi 2) + # that faults (SIGBUS) whenever the object lands 4-byte-aligned, + # blanking the viewer. With arm_use_neon=false the general C++ path + # compiles -mfpu=vfpv3-d16 (no NEON block store), while codec/ + # graphics (libvpx, Skia) re-add -mfpu=neon per-file with runtime + # detection, so hardware SIMD decode is preserved. Appended after + # fetch_qt because rsync --delete restores the pristine .pri. + echo 'gn_args += arm_use_neon=false' \ + >> "/src/qt$QT_MAJOR/qtwebengine/src/core/config/common.pri" + if [ "${CLEAN_BUILD-x}" == "1" ]; then rm -rf "$SRC_DIR" fi diff --git a/src/anthias_webview/pyshim/cgi.py b/src/anthias_webview/pyshim/cgi.py new file mode 100644 index 000000000..ffde19c9e --- /dev/null +++ b/src/anthias_webview/pyshim/cgi.py @@ -0,0 +1,11 @@ +"""Shim for the stdlib ``cgi`` module, removed in Python 3.13 (Debian +trixie). Chromium 87 build tooling uses only ``cgi.escape`` (dropped from +the stdlib back in 3.8 in favour of ``html.escape``). +""" + + +def escape(s, quote=False): + s = s.replace('&', '&').replace('<', '<').replace('>', '>') + if quote: + s = s.replace('"', '"') + return s diff --git a/src/anthias_webview/pyshim/imp.py b/src/anthias_webview/pyshim/imp.py new file mode 100644 index 000000000..62581414f --- /dev/null +++ b/src/anthias_webview/pyshim/imp.py @@ -0,0 +1,138 @@ +"""Minimal ``imp`` shim for Python 3.12+ (the stdlib ``imp`` module was +removed in 3.12). Debian trixie ships Python 3.13, but Chromium 87's +build tooling (mojo codegen, bundled jinja2, build/print_python_deps.py) +still ``import imp``. This shim reimplements the small subset those tools +use on top of ``importlib`` so the Qt 5.15 WebEngine build runs on a +modern interpreter. Placed on PYTHONPATH by build_qt5.sh. +""" + +import importlib +import importlib.machinery +import importlib.util +import os +import sys +import types + +# Old imp.* file-type constants (values match CPython's historical ones). +PY_SOURCE = 1 +PY_COMPILED = 2 +C_EXTENSION = 3 +PKG_DIRECTORY = 5 +C_BUILTIN = 6 +PY_FROZEN = 7 +SEARCH_ERROR = 0 + + +def new_module(name): + return types.ModuleType(name) + + +def get_magic(): + return importlib.util.MAGIC_NUMBER + + +def cache_from_source(path, debug_override=None): + return importlib.util.cache_from_source(path, debug_override=debug_override) + + +def source_from_cache(path): + return importlib.util.source_from_cache(path) + + +def acquire_lock(): + pass + + +def release_lock(): + pass + + +def lock_held(): + return False + + +def get_suffixes(): + ext = [(s, 'rb', C_EXTENSION) + for s in importlib.machinery.EXTENSION_SUFFIXES] + src = [(s, 'r', PY_SOURCE) + for s in importlib.machinery.SOURCE_SUFFIXES] + byt = [(s, 'rb', PY_COMPILED) + for s in importlib.machinery.BYTECODE_SUFFIXES] + return ext + src + byt + + +def load_source(name, pathname, file=None): + loader = importlib.machinery.SourceFileLoader(name, pathname) + spec = importlib.util.spec_from_file_location(name, pathname, loader=loader) + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + loader.exec_module(module) + return module + + +def load_dynamic(name, pathname, file=None): + loader = importlib.machinery.ExtensionFileLoader(name, pathname) + spec = importlib.util.spec_from_file_location(name, pathname, loader=loader) + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + loader.exec_module(module) + return module + + +def find_module(name, path=None): + if path is None: + path = sys.path + spec = importlib.machinery.PathFinder.find_spec(name, path) + if spec is None: + raise ImportError('No module named %r' % name, name=name) + origin = spec.origin + if spec.submodule_search_locations is not None: + loc = list(spec.submodule_search_locations) + return (None, loc[0] if loc else origin, ('', '', PKG_DIRECTORY)) + suffix = os.path.splitext(origin)[1] if origin else '' + if suffix in importlib.machinery.EXTENSION_SUFFIXES: + kind, mode = C_EXTENSION, 'rb' + elif suffix in importlib.machinery.BYTECODE_SUFFIXES: + kind, mode = PY_COMPILED, 'rb' + else: + kind, mode = PY_SOURCE, 'r' + handle = open(origin, mode) if origin else None + return (handle, origin, (suffix, mode, kind)) + + +def load_module(name, file, pathname, description): + _suffix, _mode, kind = description + try: + if kind == PY_SOURCE: + return load_source(name, pathname, file) + if kind == C_EXTENSION: + return load_dynamic(name, pathname, file) + if kind == PKG_DIRECTORY: + init = os.path.join(pathname, '__init__.py') + spec = importlib.util.spec_from_file_location(name, init) + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + spec.loader.exec_module(module) + return module + finally: + if file: + try: + file.close() + except Exception: + pass + return importlib.import_module(name) + + +def reload(module): + return importlib.reload(module) + + +class NullImporter: + def __init__(self, path): + if path == '': + raise ImportError('empty pathname', path='') + if os.path.isdir(path): + raise ImportError('existing directory', path=path) + + def find_module(self, fullname): + return None diff --git a/src/anthias_webview/pyshim/pipes.py b/src/anthias_webview/pyshim/pipes.py new file mode 100644 index 000000000..7c6d4758d --- /dev/null +++ b/src/anthias_webview/pyshim/pipes.py @@ -0,0 +1,6 @@ +"""Shim for the stdlib ``pipes`` module, removed in Python 3.13 (Debian +trixie). Chromium 87's build tooling (build/android/gyp/util/build_utils.py) +uses only ``pipes.quote``, which moved to ``shlex.quote``. +""" + +from shlex import quote # noqa: F401 diff --git a/tools/image_builder/utils.py b/tools/image_builder/utils.py index 2902da3e2..c33864d89 100644 --- a/tools/image_builder/utils.py +++ b/tools/image_builder/utils.py @@ -174,7 +174,7 @@ def get_viewer_context(board: str, target_platform: str) -> dict[str, Any]: # straight from Debian apt (qt6-*-dev in viewer_extra_apt below). qt_version = '6.4.2' if is_qt6 else '5.15.19' qt_major_version = qt_version.split('.')[0] - qt5_toolchain_url = f'{releases_url}/WebView-v2026.07.0' + qt5_toolchain_url = f'{releases_url}/WebView-v2026.07.1' # Viewer-only apt deps. The shared runtime set (cec-utils, curl, # ffmpeg, git, libcec7, procps, psmisc, python-is-python3, From f714587f5b37a5a58d528092e2e370d0569d9b95 Mon Sep 17 00:00:00 2001 From: Viktor Petersson Date: Fri, 10 Jul 2026 19:58:50 +0000 Subject: [PATCH 2/8] chore(lint): exclude build-time pyshim shims from ruff/mypy The imp/pipes/cgi shims run only inside the Qt 5 toolchain builder image to satisfy Chromium 87's Python-2-era build tooling on trixie's Python 3.13; they emulate removed-stdlib APIs, so the app's strict typing and formatting rules don't apply. Same treatment as the sibling sysroot-relativelinks.py already gets. Co-Authored-By: Claude Opus 4.8 (1M context) --- pyproject.toml | 3 +++ ruff.toml | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index ac16e9cce..03d5fde59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -228,6 +228,9 @@ exclude = [ # the file header. Adding type annotations would put the file # off-pin and silently break the provenance comment. "^src/anthias_webview/sysroot-relativelinks\\.py$", + # Build-time-only stdlib shims (imp/pipes/cgi) for Chromium 87's + # Python-2-era build tooling; not app code, emulate removed stdlib. + "^src/anthias_webview/pyshim/", ] [[tool.mypy.overrides]] diff --git a/ruff.toml b/ruff.toml index 638577b8c..1d9bd7a1a 100644 --- a/ruff.toml +++ b/ruff.toml @@ -6,6 +6,11 @@ exclude = [ # the file header. Reformatting would put the file off-pin and # silently break the provenance comment. "src/anthias_webview/sysroot-relativelinks.py", + # Build-time-only stdlib shims (imp/pipes/cgi) copied into the Qt 5 + # toolchain builder image to satisfy Chromium 87's Python-2-era build + # tooling on Debian trixie's Python 3.13. Not app code; they emulate + # removed-stdlib APIs, so app formatting/typing rules don't apply. + "src/anthias_webview/pyshim/*.py", ] target-version = "py313" From 83f4dadf76c3f1543afa0f3520bbc5b850072964 Mon Sep 17 00:00:00 2001 From: Viktor Petersson Date: Fri, 10 Jul 2026 20:08:30 +0000 Subject: [PATCH 3/8] =?UTF-8?q?fix(webview):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20fail-loud=20installs,=20recursion-proof=20cc=20wrap?= =?UTF-8?q?per?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot review follow-ups on the Qt 5 toolchain build: - Drop the `2>/dev/null || true` swallowing on the pyshim copy and the six/setuptools installs — these are required on Python 3.13, so a real failure should abort the build (set -e) instead of surfacing later as a confusing ImportError inside a Chromium gn/ninja action. - Make the -mno-unaligned-access compiler wrapper recursion-proof: move the original aside to .real before writing the wrapper, so it can never exec itself if arm-linux-gnueabihf-{gcc,g++} is a real binary rather than an alternatives symlink. - imp shim: correct the docstring (copied into site-packages, not PYTHONPATH) and give NullImporter.find_module the optional legacy `path` argument. - cgi shim: document that escape() deliberately mirrors the stdlib cgi.escape (double-quote only), not html.escape. - utils: fix the stale WebView-v2026.07.0 comment (now 07.1). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/anthias_webview/build_qt5.sh | 39 ++++++++++++++++++------------- src/anthias_webview/pyshim/cgi.py | 5 ++++ src/anthias_webview/pyshim/imp.py | 7 ++++-- tools/image_builder/utils.py | 6 +++-- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/src/anthias_webview/build_qt5.sh b/src/anthias_webview/build_qt5.sh index ab4c222bb..e9a23a9f8 100755 --- a/src/anthias_webview/build_qt5.sh +++ b/src/anthias_webview/build_qt5.sh @@ -28,26 +28,32 @@ DEBIAN_VERSION=$(lsb_release -cs) # the py3-only shim and break it ("cannot import name quote"). python3's # site-packages is only consulted after the stdlib, so it fills the gaps # for removed modules without shadowing anything that still exists. +# These shims are required on Python 3.13, so let a failed copy abort the +# build (set -e) rather than surface later as a confusing ImportError deep +# inside a Chromium gn/ninja action. if [ -d /webview/pyshim ]; then - PY3_SITE="$(python3 -c 'import sysconfig; print(sysconfig.get_path("purelib"))' 2>/dev/null || echo /usr/lib/python3/dist-packages)" - cp -f /webview/pyshim/*.py "$PY3_SITE/" 2>/dev/null || true + PY3_SITE="$(python3 -c 'import sysconfig; print(sysconfig.get_path("purelib"))')" + cp -f /webview/pyshim/*.py "$PY3_SITE/" fi # Chromium 87's grit/build tooling `import six` (and six.moves); trixie's # python3.13 ships no six by default, so the gn/ninja codegen actions # abort with "No module named 'six.moves'". Install the distro package -# (six 1.17 supports 3.13) once at the start of the build. +# (six 1.17 supports 3.13); fall back to pip. Both failing is fatal (no +# trailing `|| true`) — six is required for gn/grit. if ! python3 -c 'import six.moves' 2>/dev/null; then - apt-get update -qq && apt-get install -y -qq python3-six \ - || pip3 install --break-system-packages six || true + apt-get update -qq + apt-get install -y -qq python3-six \ + || pip3 install --break-system-packages six fi # Chromium 87's licenses/gyp tooling imports distutils (spawn, version, # dir_util, archive_util), removed from the stdlib in Python 3.12. -# setuptools re-provides it via its distutils-precedence .pth, so just -# ensure setuptools is installed. +# setuptools re-provides it via its distutils-precedence .pth. Required, so +# both apt and pip failing is fatal. if ! python3 -c 'import distutils.spawn' 2>/dev/null; then - apt-get update -qq && apt-get install -y -qq python3-setuptools \ - || pip3 install --break-system-packages setuptools || true + apt-get update -qq + apt-get install -y -qq python3-setuptools \ + || pip3 install --break-system-packages setuptools fi # Force -mno-unaligned-access on EVERY armhf compile. Modern Debian gcc @@ -61,16 +67,17 @@ fi # covers only Chromium's gn C++; Qt's own libs (libQt5Core/Gui/Qml/Quick, # built by qmake with -mfpu=neon-vfpv4) and the WebEngine integration layer # still emit it, so wrap the compiler itself — both qmake and gn resolve -# arm-linux-gnueabihf-{gcc,g++} through /usr/bin. Resolve the real binary -# BEFORE replacing the symlink so the wrapper doesn't recurse. +# arm-linux-gnueabihf-{gcc,g++} through /usr/bin. Move the original aside +# to .real first so the wrapper never execs itself (safe whether the +# original is an alternatives symlink or a real binary). for _t in gcc g++; do - _real="$(readlink -f "/usr/bin/arm-linux-gnueabihf-$_t")" - rm -f "/usr/bin/arm-linux-gnueabihf-$_t" - cat > "/usr/bin/arm-linux-gnueabihf-$_t" < "$_bin" <', '>') if quote: s = s.replace('"', '"') diff --git a/src/anthias_webview/pyshim/imp.py b/src/anthias_webview/pyshim/imp.py index 62581414f..f9346f939 100644 --- a/src/anthias_webview/pyshim/imp.py +++ b/src/anthias_webview/pyshim/imp.py @@ -3,7 +3,10 @@ build tooling (mojo codegen, bundled jinja2, build/print_python_deps.py) still ``import imp``. This shim reimplements the small subset those tools use on top of ``importlib`` so the Qt 5.15 WebEngine build runs on a -modern interpreter. Placed on PYTHONPATH by build_qt5.sh. +modern interpreter. build_qt5.sh copies this into python3's +site-packages (NOT onto a shared PYTHONPATH, which would also shadow +python2.7's real ``imp`` for the codegen actions that still run under +python2). """ import importlib @@ -134,5 +137,5 @@ def __init__(self, path): if os.path.isdir(path): raise ImportError('existing directory', path=path) - def find_module(self, fullname): + def find_module(self, fullname, path=None): return None diff --git a/tools/image_builder/utils.py b/tools/image_builder/utils.py index c33864d89..239c8cabf 100644 --- a/tools/image_builder/utils.py +++ b/tools/image_builder/utils.py @@ -170,8 +170,10 @@ def get_viewer_context(board: str, target_platform: str) -> dict[str, Any]: # Qt version is only relevant for the Qt 5 path: pi2/pi3 pull the # cross-built Qt 5 toolchain tarball at build time. Qt 5 is frozen # for these boards, so the toolchain stays pinned to the - # WebView-v2026.07.0 release indefinitely. Qt 6 boards install Qt - # straight from Debian apt (qt6-*-dev in viewer_extra_apt below). + # WebView-v2026.07.1 release (bumped from 07.0 to carry the armv7 + # NEON-alignment fix — see bin/rebuild_qt5_toolchain.sh / + # src/anthias_webview/build_qt5.sh). Qt 6 boards install Qt straight + # from Debian apt (qt6-*-dev in viewer_extra_apt below). qt_version = '6.4.2' if is_qt6 else '5.15.19' qt_major_version = qt_version.split('.')[0] qt5_toolchain_url = f'{releases_url}/WebView-v2026.07.1' From 1dab0f691445a80602703e471b08dde553d0cc35 Mon Sep 17 00:00:00 2001 From: Viktor Petersson Date: Fri, 10 Jul 2026 20:18:17 +0000 Subject: [PATCH 4/8] fix(webview): harden toolchain build per review round 2 - pyshim copy: guard on the glob matching real files so an empty/absent dir doesn't hand cp a literal `*.py` and abort under set -e. - -mno-unaligned-access wrapper: only wrap when the tool exists and isn't already wrapped (idempotent; doesn't pre-empt fetch_cross_compile_tool's own presence check). - arm_use_neon=false: grep-guard the common.pri append so a rebuild in the same workdir can't duplicate the line. - Bump the remaining WebView-v2026.07.0 references (docs, scripts, github release-filter test fixtures) to 07.1 for consistency with the pinned toolchain release. Co-Authored-By: Claude Opus 4.8 (1M context) --- bin/rebuild_qt5_toolchain.sh | 4 ++-- docker/Dockerfile.qt5-webview-builder.j2 | 6 ++--- src/anthias_server/lib/github.py | 4 ++-- src/anthias_webview/README.md | 4 ++-- src/anthias_webview/build_qt5.sh | 28 +++++++++++++++--------- tests/test_github.py | 6 ++--- 6 files changed, 30 insertions(+), 22 deletions(-) diff --git a/bin/rebuild_qt5_toolchain.sh b/bin/rebuild_qt5_toolchain.sh index 39e550bbb..e5cd19ed3 100755 --- a/bin/rebuild_qt5_toolchain.sh +++ b/bin/rebuild_qt5_toolchain.sh @@ -9,7 +9,7 @@ # This is an out-of-band prereq: docker/Dockerfile.qt5-webview-builder.j2 # fetches these tarballs from the release the viewer image references # via tools/image_builder/utils.py (qt5_toolchain_url, currently -# WebView-v2026.07.0). If that release doesn't have trixie-* artifacts +# WebView-v2026.07.1). If that release doesn't have trixie-* artifacts # for both pi2 and pi3, pi2/pi3 viewer image builds fail with # sha256sum: no properly formatted checksum lines found # (the curl on the missing .sha256 falls back to a 404 HTML page). @@ -132,7 +132,7 @@ echo " (cd '${OUT_DIR}' && sha256sum -c qt5-5.15.19-trixie-*.tar.gz.sha256)" echo echo "Upload to a WebView-v* release. If you re-use the tag the viewer" echo "image references in tools/image_builder/utils.py via qt5_toolchain_url" -echo "(currently WebView-v2026.07.0), no source change is needed;" +echo "(currently WebView-v2026.07.1), no source change is needed;" echo "otherwise bump that URL to the new tag in the same commit." echo " gh release upload \\" echo " '${OUT_DIR}'/qt5-5.15.19-trixie-pi2.tar.gz{,.sha256} \\" diff --git a/docker/Dockerfile.qt5-webview-builder.j2 b/docker/Dockerfile.qt5-webview-builder.j2 index 2f6c45990..85e401603 100644 --- a/docker/Dockerfile.qt5-webview-builder.j2 +++ b/docker/Dockerfile.qt5-webview-builder.j2 @@ -8,11 +8,11 @@ at via `-sysroot /sysroot`. 2. webview-builder — host x86 (`$BUILDPLATFORM`); installs the Linaro gcc-7.4.1 cross-compiler plus the pre-built Qt 5 - toolchain pinned to WebView-v2026.07.0, then compiles the + toolchain pinned to WebView-v2026.07.1, then compiles the in-tree src/anthias_webview/ source. Qt 5 is frozen for pi2/pi3 boards, so the toolchain artifact at - WebView-v2026.07.0 never changes. To CVE-patch the toolchain run + WebView-v2026.07.1 never changes. To CVE-patch the toolchain run bin/rebuild_qt5_toolchain.sh and re-upload to that tag. #} FROM --platform=linux/arm/v7 {{ base_image }}:{{ base_image_tag }} AS qt5-sysroot @@ -214,7 +214,7 @@ RUN mkdir -p /src/gcc-linaro-7.4.1-2019.02-x86_64_arm-linux-gnueabihf/bin && \ "/src/gcc-linaro-7.4.1-2019.02-x86_64_arm-linux-gnueabihf/bin/$(basename "$tool")"; \ done -# Pre-built Qt 5 toolchain — pinned permanently to WebView-v2026.07.0 +# Pre-built Qt 5 toolchain — pinned permanently to WebView-v2026.07.1 # (Qt 5 is frozen for pi2/pi3). The qt5pi/ tree contains both host # qmake (x86) and armhf libs the webview app links against. RUN mkdir -p /src/{{ artifact_board }} && \ diff --git a/src/anthias_server/lib/github.py b/src/anthias_server/lib/github.py index 3db27236c..72638d321 100644 --- a/src/anthias_server/lib/github.py +++ b/src/anthias_server/lib/github.py @@ -33,7 +33,7 @@ # The releases *list*, not ``/releases/latest``: that endpoint returns # the newest non-draft, non-prerelease release of ANY kind, and the # repo also publishes non-app releases (e.g. the frozen Qt 5 toolchain, -# tagged ``WebView-v2026.07.0``). The day such a release goes out, +# tagged ``WebView-v2026.07.1``). The day such a release goes out, # ``/releases/latest`` stops pointing at an Anthias CalVer tag and the # update check degrades fleet-wide until the next app release (Sentry # ANTHIAS-3P). Listing lets us pick the highest CalVer-parseable tag @@ -81,7 +81,7 @@ def _latest_parseable_tag(payload: object) -> str | None: ``/releases`` list payload. Skips drafts, prereleases, and any release whose tag doesn't parse - (non-app releases like ``WebView-v2026.07.0``, hand-edited tags + (non-app releases like ``WebView-v2026.07.1``, hand-edited tags like ``nightly``). Highest-by-version rather than first-in-list: the list is ordered by creation date, and a non-app release created after the newest app release would otherwise mask it. diff --git a/src/anthias_webview/README.md b/src/anthias_webview/README.md index 74404765c..beff470e5 100644 --- a/src/anthias_webview/README.md +++ b/src/anthias_webview/README.md @@ -25,7 +25,7 @@ viewer image build: Qt 5 toolchain, all in `docker/Dockerfile.qt5-webview-builder.j2`. Qt 5 is frozen for these boards, so the toolchain itself is a permanent artifact at - the `WebView-v2026.07.0` GitHub release. + the `WebView-v2026.07.1` GitHub release. To rebuild a viewer image (which rebuilds the binary): @@ -39,7 +39,7 @@ uv run python -m tools.image_builder \ If the Qt 5 toolchain itself needs to change (CVE patch, base image bump), `bin/rebuild_qt5_toolchain.sh` produces fresh `qt5-5.15.19-trixie-{pi2,pi3}.tar.gz` tarballs. Re-uploading them to -the same `WebView-v2026.07.0` release tag means no source change is +the same `WebView-v2026.07.1` release tag means no source change is needed elsewhere; uploading to a new tag means bumping `qt5_toolchain_url` in `tools/image_builder/utils.py`. Runtime is ~2-4 hours per board on a beefy x86 host (Qt 5 + QtWebEngine under diff --git a/src/anthias_webview/build_qt5.sh b/src/anthias_webview/build_qt5.sh index e9a23a9f8..d2ca4b88e 100755 --- a/src/anthias_webview/build_qt5.sh +++ b/src/anthias_webview/build_qt5.sh @@ -31,7 +31,9 @@ DEBIAN_VERSION=$(lsb_release -cs) # These shims are required on Python 3.13, so let a failed copy abort the # build (set -e) rather than surface later as a confusing ImportError deep # inside a Chromium gn/ninja action. -if [ -d /webview/pyshim ]; then +# `ls` guard so an empty (or absent) pyshim dir doesn't feed cp a literal +# unexpanded glob and abort the build under set -e. +if ls /webview/pyshim/*.py >/dev/null 2>&1; then PY3_SITE="$(python3 -c 'import sysconfig; print(sysconfig.get_path("purelib"))')" cp -f /webview/pyshim/*.py "$PY3_SITE/" fi @@ -69,17 +71,21 @@ fi # still emit it, so wrap the compiler itself — both qmake and gn resolve # arm-linux-gnueabihf-{gcc,g++} through /usr/bin. Move the original aside # to .real first so the wrapper never execs itself (safe whether the -# original is an alternatives symlink or a real binary). +# original is an alternatives symlink or a real binary). Guarded on the +# tool existing and not already being wrapped, so it's idempotent and +# doesn't fail before fetch_cross_compile_tool's own presence check. for _t in gcc g++; do _bin="/usr/bin/arm-linux-gnueabihf-$_t" - mv "$_bin" "$_bin.real" - cat > "$_bin" < "$_bin" <> "/src/qt$QT_MAJOR/qtwebengine/src/core/config/common.pri" + # fetch_qt because rsync --delete restores the pristine .pri; + # grep-guarded so a re-run in the same workdir doesn't duplicate it. + _common_pri="/src/qt$QT_MAJOR/qtwebengine/src/core/config/common.pri" + grep -qF 'arm_use_neon=false' "$_common_pri" \ + || echo 'gn_args += arm_use_neon=false' >> "$_common_pri" if [ "${CLEAN_BUILD-x}" == "1" ]; then rm -rf "$SRC_DIR" @@ -367,7 +375,7 @@ function build_qt () { # (docker/Dockerfile.qt5-webview-builder.j2 includes this Qt 5 # toolchain at build time). This script only emits the toolchain # tarball — bin/rebuild_qt5_toolchain.sh uploads it to the frozen - # WebView-v2026.07.0 release. + # WebView-v2026.07.1 release. } # Modify paths for build process diff --git a/tests/test_github.py b/tests/test_github.py index 1976688e8..0ca0bea6e 100644 --- a/tests/test_github.py +++ b/tests/test_github.py @@ -93,14 +93,14 @@ def test_fetch_latest_release_tag_skips_non_app_releases( github_env: None, redis_data: dict[str, str] ) -> None: """The ANTHIAS-3P scenario: a non-app release (frozen Qt 5 - toolchain, tagged ``WebView-v2026.07.0``) is the newest release in + toolchain, tagged ``WebView-v2026.07.1``) is the newest release in the repo. ``/releases/latest`` would return it and break the update check fleet-wide; the list-based fetch must skip it and pick the newest parseable CalVer tag instead.""" resp = _resp( 200, json_data=[ - {'tag_name': 'WebView-v2026.07.0', 'name': 'WebView toolchain'}, + {'tag_name': 'WebView-v2026.07.1', 'name': 'WebView toolchain'}, {'tag_name': 'v2026.7.0', 'name': 'Anthias'}, {'tag_name': 'v2026.6.3', 'name': 'Anthias'}, ], @@ -181,7 +181,7 @@ def test_fetch_latest_release_tag_invalid_json( [], [{'name': 'Release without tag_name'}], [{'tag_name': 12345}], - [{'tag_name': 'nightly'}, {'tag_name': 'WebView-v2026.07.0'}], + [{'tag_name': 'nightly'}, {'tag_name': 'WebView-v2026.07.1'}], {'tag_name': 'v2026.6.0'}, # dict (old /latest shape), not a list ], ids=[ From 162c848242644d9e97d0582dbef3b38da66df1df Mon Sep 17 00:00:00 2001 From: Viktor Petersson Date: Fri, 10 Jul 2026 20:23:22 +0000 Subject: [PATCH 5/8] fix(webview): drop illusory pip3 fallback in toolchain build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Qt5 builder image installs python3 but not python3-pip, so the `|| pip3 install ...` fallback for six/setuptools could never actually run — if the apt install fails the pip3 call just errors with command-not-found. Install via apt only and let a real failure abort the build (set -e), which is the honest behaviour. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/anthias_webview/build_qt5.sh | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/anthias_webview/build_qt5.sh b/src/anthias_webview/build_qt5.sh index d2ca4b88e..4e028eaf9 100755 --- a/src/anthias_webview/build_qt5.sh +++ b/src/anthias_webview/build_qt5.sh @@ -41,21 +41,20 @@ fi # Chromium 87's grit/build tooling `import six` (and six.moves); trixie's # python3.13 ships no six by default, so the gn/ninja codegen actions # abort with "No module named 'six.moves'". Install the distro package -# (six 1.17 supports 3.13); fall back to pip. Both failing is fatal (no -# trailing `|| true`) — six is required for gn/grit. +# (six 1.17 supports 3.13). No pip fallback — the builder image has apt +# but not necessarily python3-pip, and a failed apt install should abort +# the build (set -e) rather than pretend a missing pip3 could recover it. if ! python3 -c 'import six.moves' 2>/dev/null; then apt-get update -qq - apt-get install -y -qq python3-six \ - || pip3 install --break-system-packages six + apt-get install -y -qq python3-six fi # Chromium 87's licenses/gyp tooling imports distutils (spawn, version, # dir_util, archive_util), removed from the stdlib in Python 3.12. -# setuptools re-provides it via its distutils-precedence .pth. Required, so -# both apt and pip failing is fatal. +# setuptools re-provides it via its distutils-precedence .pth. apt-only for +# the same reason as six above. if ! python3 -c 'import distutils.spawn' 2>/dev/null; then apt-get update -qq - apt-get install -y -qq python3-setuptools \ - || pip3 install --break-system-packages setuptools + apt-get install -y -qq python3-setuptools fi # Force -mno-unaligned-access on EVERY armhf compile. Modern Debian gcc From 99ef596bc82199e1b1ff2ea77ba506cfd33e4cd3 Mon Sep 17 00:00:00 2001 From: Viktor Petersson Date: Fri, 10 Jul 2026 20:28:45 +0000 Subject: [PATCH 6/8] =?UTF-8?q?fix(webview):=20imp=20shim=20find=5Fmodule?= =?UTF-8?q?=20=E2=80=94=20don't=20open=20built-in/frozen=20origins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit find_module() computed a file handle from spec.origin unconditionally; for built-in/frozen modules origin is the sentinel 'built-in'/'frozen' (and None for namespace packages), so open() would fail. Only open a genuine file-backed origin. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/anthias_webview/pyshim/imp.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/anthias_webview/pyshim/imp.py b/src/anthias_webview/pyshim/imp.py index f9346f939..9a745bda1 100644 --- a/src/anthias_webview/pyshim/imp.py +++ b/src/anthias_webview/pyshim/imp.py @@ -92,14 +92,18 @@ def find_module(name, path=None): if spec.submodule_search_locations is not None: loc = list(spec.submodule_search_locations) return (None, loc[0] if loc else origin, ('', '', PKG_DIRECTORY)) - suffix = os.path.splitext(origin)[1] if origin else '' + # A real file-backed module has a path origin; built-in/frozen modules + # use the sentinels 'built-in'/'frozen' (and namespace packages None) — + # never open() those. + is_file = bool(origin) and origin not in ('built-in', 'frozen') + suffix = os.path.splitext(origin)[1] if is_file else '' if suffix in importlib.machinery.EXTENSION_SUFFIXES: kind, mode = C_EXTENSION, 'rb' elif suffix in importlib.machinery.BYTECODE_SUFFIXES: kind, mode = PY_COMPILED, 'rb' else: kind, mode = PY_SOURCE, 'r' - handle = open(origin, mode) if origin else None + handle = open(origin, mode) if is_file else None return (handle, origin, (suffix, mode, kind)) From 17c7408c395d20a0a84aed37dddadf99d28728ae Mon Sep 17 00:00:00 2001 From: Viktor Petersson Date: Fri, 10 Jul 2026 20:33:43 +0000 Subject: [PATCH 7/8] fix(webview): fail loud on vendored-six overwrite The vendored-six overwrite swallowed all errors (2>/dev/null || true). six is installed earlier in the script so the import can't fail, and a silently-failed overwrite would only resurface later as the exact "No module named 'six.moves'" grit crash this step exists to prevent. Drop the swallowing and fold the two finds into one. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/anthias_webview/build_qt5.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/anthias_webview/build_qt5.sh b/src/anthias_webview/build_qt5.sh index 4e028eaf9..d66a7b0b9 100755 --- a/src/anthias_webview/build_qt5.sh +++ b/src/anthias_webview/build_qt5.sh @@ -233,14 +233,14 @@ function build_qt () { # six 1.10 as a *package* (six/__init__.py) there, so overwrite # both the six.py files and every six/ package __init__.py with # the distro copy. - SYS_SIX="$(python3 -c 'import six, sys; sys.stdout.write(six.__file__)' 2>/dev/null || true)" - if [ -n "$SYS_SIX" ] && [ -f "$SYS_SIX" ]; then - CHROMIUM_DIR="/src/qt$QT_MAJOR/qtwebengine/src/3rdparty/chromium" - find "$CHROMIUM_DIR" -path '*/six/six.py' -print \ - -exec cp "$SYS_SIX" {} \; 2>/dev/null || true - find "$CHROMIUM_DIR" -path '*/six/__init__.py' -print \ - -exec cp "$SYS_SIX" {} \; 2>/dev/null || true - fi + # No error swallowing: six was installed above, so the import must + # succeed, and a failed overwrite would only resurface later as the + # "No module named 'six.moves'" grit failure this is meant to fix. + SYS_SIX="$(python3 -c 'import six, sys; sys.stdout.write(six.__file__)')" + CHROMIUM_DIR="/src/qt$QT_MAJOR/qtwebengine/src/3rdparty/chromium" + find "$CHROMIUM_DIR" \ + \( -path '*/six/six.py' -o -path '*/six/__init__.py' \) \ + -print -exec cp "$SYS_SIX" {} \; # Force arm_use_neon=false for the Chromium build (Yocto's fix: # OSSystems meta-chromium / lgsvl meta-lgsvl-browser). QtWebEngine From 4d88f5ab3b6814bd25c2d6ed39609da86a9fd336 Mon Sep 17 00:00:00 2001 From: Viktor Petersson Date: Sat, 11 Jul 2026 06:23:46 +0000 Subject: [PATCH 8/8] =?UTF-8?q?fix(webview):=20imp=20shim=20=E2=80=94=20fa?= =?UTF-8?q?ithful=20built-in/frozen=20+=20namespace-package=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address Copilot review round 7 on the Python 3.13 `imp` shim: - find_module() now resolves built-in and frozen modules the way the historical stdlib imp did when called with no explicit path: check sys.builtin_module_names / FrozenImporter first (no import side effects) and return C_BUILTIN / PY_FROZEN with a None pathname instead of leaking importlib's 'built-in'/'frozen' sentinel through as a fake filename. PathFinder alone never surfaced these (they're on no path), so it previously raised ImportError for e.g. `sys`. - load_module() no longer assumes every PKG_DIRECTORY has an __init__.py: namespace packages (which find_module also classifies as PKG_DIRECTORY) now fall through to importlib.import_module instead of exec'ing a non-existent /__init__.py and raising. Verified against built-in (sys), frozen (_frozen_importlib), regular package (json), source module, and a synthetic namespace package. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/anthias_webview/pyshim/imp.py | 39 +++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/anthias_webview/pyshim/imp.py b/src/anthias_webview/pyshim/imp.py index 9a745bda1..30f82ff10 100644 --- a/src/anthias_webview/pyshim/imp.py +++ b/src/anthias_webview/pyshim/imp.py @@ -84,6 +84,16 @@ def load_dynamic(name, pathname, file=None): def find_module(name, path=None): if path is None: + # Historical imp.find_module with no explicit path also resolves + # built-in and frozen modules, not just sys.path entries. Check + # those first (no import side effects) and return the matching kind + # with no handle and no pathname — never a fake 'built-in'/'frozen' + # filename; load_module() then defers to importlib to import them. + # (PathFinder alone can't find these — they aren't on any path.) + if name in sys.builtin_module_names: + return (None, None, ('', '', C_BUILTIN)) + if importlib.machinery.FrozenImporter.find_spec(name) is not None: + return (None, None, ('', '', PY_FROZEN)) path = sys.path spec = importlib.machinery.PathFinder.find_spec(name, path) if spec is None: @@ -92,10 +102,9 @@ def find_module(name, path=None): if spec.submodule_search_locations is not None: loc = list(spec.submodule_search_locations) return (None, loc[0] if loc else origin, ('', '', PKG_DIRECTORY)) - # A real file-backed module has a path origin; built-in/frozen modules - # use the sentinels 'built-in'/'frozen' (and namespace packages None) — - # never open() those. - is_file = bool(origin) and origin not in ('built-in', 'frozen') + # PathFinder only yields file-backed modules here (namespace packages + # were handled above via submodule_search_locations). + is_file = bool(origin) suffix = os.path.splitext(origin)[1] if is_file else '' if suffix in importlib.machinery.EXTENSION_SUFFIXES: kind, mode = C_EXTENSION, 'rb' @@ -115,12 +124,22 @@ def load_module(name, file, pathname, description): if kind == C_EXTENSION: return load_dynamic(name, pathname, file) if kind == PKG_DIRECTORY: - init = os.path.join(pathname, '__init__.py') - spec = importlib.util.spec_from_file_location(name, init) - module = importlib.util.module_from_spec(spec) - sys.modules[name] = module - spec.loader.exec_module(module) - return module + # find_module() also classifies namespace packages (no + # __init__.py) as PKG_DIRECTORY. Only exec an __init__.py when + # one actually exists and yields a loadable spec; otherwise fall + # through to importlib, which resolves regular and namespace + # packages correctly (the old imp couldn't load the latter). + init = os.path.join(pathname, '__init__.py') if pathname else None + spec = ( + importlib.util.spec_from_file_location(name, init) + if init and os.path.isfile(init) + else None + ) + if spec is not None and spec.loader is not None: + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + spec.loader.exec_module(module) + return module finally: if file: try: