From fd6776aeac1e1ca6cb36e7cd67e502f8ff2b1f7d Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Tue, 7 Apr 2026 23:32:50 +0300 Subject: [PATCH] fix(build): stop stripping zeroconf/_services + add import smoke test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - build-common.sh: remove zeroconf/_services from the strip list. zeroconf's compiled Cython _listener.pyd imports from _services internally, so stripping it broke `import zeroconf` at runtime with ModuleNotFoundError — same class of bug as the numpy.linalg strip. - build-common.sh: add smoke_test_imports() that imports every top-level dependency against the stripped site-packages. Catches "we stripped something that was actually needed" regressions at build time instead of on a user's machine after install. - build-dist.sh: wire smoke test into the Linux flow (runs real imports). - build-dist-windows.sh: cross-build can't load win_amd64 .pyd files with the host python, so instead verify that the known-required submodule dirs (numpy.linalg/lib/matrixlib/ma, zeroconf._services) exist after cleanup. Fails loud if any future strip-rule removes them. --- build-common.sh | 60 ++++++++++++++++++++++++++++++++++++++++++- build-dist-windows.sh | 15 +++++++++++ build-dist.sh | 3 +++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/build-common.sh b/build-common.sh index 0dfc202..1b93b76 100644 --- a/build-common.sh +++ b/build-common.sh @@ -131,7 +131,11 @@ cleanup_site_packages() { done # ── zeroconf ───────────────────────────────────────────── - rm -rf "$sp_dir/zeroconf/_services" 2>/dev/null || true + # DO NOT strip zeroconf/_services — the compiled Cython _listener.pyd + # imports from it, and the import fails at runtime with: + # ModuleNotFoundError: No module named 'zeroconf._services' + # Same class of bug as numpy — "presumed unused" submodule is actually + # imported internally by the package's own compiled code. # ── Strip debug symbols ────────────────────────────────── if command -v strip &>/dev/null; then @@ -180,3 +184,57 @@ compile_and_strip_sources() { # __pycache__ dirs are redundant now that we have legacy .pyc files find "$target_dir" -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true } + +# ── Import smoke test ──────────────────────────────────────── +# +# Verifies that every top-level dependency that wled_controller actually +# uses can be imported from the stripped site-packages. Catches regressions +# where cleanup_site_packages removes a submodule that turns out to be +# imported internally by the package (e.g. numpy.linalg, zeroconf._services). +# Failing here is cheap; failing on a user's machine after install is not. +# +# Args: +# $1 — path to site-packages to test against +# $2 — python executable +# $3 — (optional) extra PYTHONPATH entry (e.g. app/src for wled_controller) + +smoke_test_imports() { + local sp_dir="$1" + local py_cmd="${2:-python3}" + local extra_path="${3:-}" + + echo " Running import smoke test..." + local pypath="$sp_dir" + if [ -n "$extra_path" ]; then + pypath="$extra_path:$sp_dir" + fi + + # Modules that MUST import cleanly for the app to start. + # If you add a new top-level dependency, add it here. + PYTHONPATH="$pypath" "$py_cmd" -c " +import sys +modules = [ + 'numpy', 'numpy.linalg', 'numpy.lib', 'numpy.matrixlib', + 'cv2', + 'fastapi', 'uvicorn', 'starlette', 'pydantic', + 'zeroconf', + 'PIL', 'PIL.Image', + 'yaml', +] +failed = [] +for mod in modules: + try: + __import__(mod) + except Exception as e: + failed.append(f'{mod}: {type(e).__name__}: {e}') +if failed: + print('SMOKE TEST FAILED:', file=sys.stderr) + for f in failed: + print(f' {f}', file=sys.stderr) + sys.exit(1) +print(' Smoke test passed (imported', len(modules), 'modules)') +" || { + echo " ERROR: smoke test failed — site-packages is broken, aborting build" + return 1 + } +} diff --git a/build-dist-windows.sh b/build-dist-windows.sh index a6f83e5..2e77d29 100644 --- a/build-dist-windows.sh +++ b/build-dist-windows.sh @@ -279,6 +279,21 @@ find "$SITE_PACKAGES/winrt" -name "*.pyi" -delete 2>/dev/null || true # the generated .pyc will ImportError on the target. compile_and_strip_sources "$SITE_PACKAGES" "python" +# Windows cross-build: host python can't load win_amd64 .pyd files, so +# we can't `import numpy` for real. Instead, check that the submodules +# known to be imported internally exist on disk — the same landmines that +# cost users a broken v0.0.0.dev0 installer. +echo " Verifying required submodules exist after cleanup..." +for required in \ + "numpy/linalg" "numpy/lib" "numpy/matrixlib" "numpy/ma" \ + "zeroconf/_services"; do + if [ ! -d "$SITE_PACKAGES/$required" ] && [ ! -f "$SITE_PACKAGES/$required.py" ] && [ ! -f "$SITE_PACKAGES/$required.pyc" ]; then + echo " ERROR: $required missing from site-packages — cleanup_site_packages removed something required. Aborting." + exit 1 + fi +done +echo " All required submodules present." + WHEEL_COUNT=$(ls "$WHEEL_DIR"/*.whl 2>/dev/null | wc -l) echo " Installed $WHEEL_COUNT packages" diff --git a/build-dist.sh b/build-dist.sh index 7e77a80..b35101d 100644 --- a/build-dist.sh +++ b/build-dist.sh @@ -56,6 +56,9 @@ cleanup_site_packages "$SITE_PACKAGES" "so" "so" # Pre-compile and strip .py sources (must happen AFTER cleanup) compile_and_strip_sources "$SITE_PACKAGES" "python" +# Fail loud if cleanup broke any required import +smoke_test_imports "$SITE_PACKAGES" "python" + # ── Build frontend ─────────────────────────────────────────── echo "[4/7] Building frontend..."