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..."