diff --git a/build-common.sh b/build-common.sh index 1b93b76..5b8ef80 100644 --- a/build-common.sh +++ b/build-common.sh @@ -151,13 +151,20 @@ cleanup_site_packages() { echo " Site-packages after cleanup: $cleaned_size" } -# ── Pre-compile .py → .pyc and strip sources ───────────────── +# ── Pre-compile .py → .pyc (keep sources) ──────────────────── # -# MUST run AFTER cleanup_site_packages (so we don't waste work compiling -# files that are about to be deleted) and BEFORE any step that ships the -# result. Uses `compileall -b` to produce legacy `foo.pyc` next to -# `foo.py` (not `__pycache__/foo.cpython-XX.pyc`), which survives the -# __pycache__ cleanup and works without a matching .py file at import. +# MUST run AFTER cleanup_site_packages. Speeds up first startup by +# producing .pyc alongside .py. We deliberately do NOT delete .py +# sources afterwards: +# +# 1. OpenCV's loader does literal file I/O on cv2/config.py (not +# an import) — stripping it breaks `import cv2` with: +# "OpenCV loader: missing configuration file: ['config.py']". +# 2. Other packages may do similar tricks (inspect.getsource, +# runtime introspection, __file__-relative data loading). +# 3. The size saving (~30%) isn't worth the whack-a-mole of +# shipping broken installers. We already hit this with +# numpy.linalg and zeroconf._services — enough incidents. # # Args: # $1 — directory to compile (site-packages or app/src) @@ -176,12 +183,9 @@ compile_and_strip_sources() { echo " ERROR: compileall failed for $target_dir — aborting" return 1 } - - echo " Removing .py source (keeping .pyc)..." - # Keep __init__.py so package discovery still works in edge cases - # where namespace detection checks for the file. - find "$target_dir" -name "*.py" ! -name "__init__.py" -delete 2>/dev/null || true - # __pycache__ dirs are redundant now that we have legacy .pyc files + # Drop __pycache__ to save the duplicated PEP-3147 copies; the + # `-b` flag above placed legacy .pyc next to each .py, so nothing + # of value is lost here. find "$target_dir" -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true } @@ -209,32 +213,57 @@ smoke_test_imports() { 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 " + # Modules that MUST import cleanly IF PRESENT. We don't enforce + # installation — Pillow for example is only a Windows dep. But if a + # module's top-level package dir exists in site-packages and we + # can't import it, that's a broken install and we abort. + PYTHONPATH="$pypath" "$py_cmd" - "$sp_dir" <<'PYEOF' || { + echo " ERROR: smoke test failed — site-packages is broken, aborting build" + return 1 + } +import importlib +import os import sys -modules = [ - 'numpy', 'numpy.linalg', 'numpy.lib', 'numpy.matrixlib', - 'cv2', - 'fastapi', 'uvicorn', 'starlette', 'pydantic', - 'zeroconf', - 'PIL', 'PIL.Image', - 'yaml', + +sp_dir = sys.argv[1] + +# (module_name, site-packages path to check for presence) +candidates = [ + ('numpy', 'numpy'), + ('numpy.linalg', 'numpy/linalg'), + ('numpy.lib', 'numpy/lib'), + ('numpy.matrixlib', 'numpy/matrixlib'), + ('cv2', 'cv2'), + ('fastapi', 'fastapi'), + ('uvicorn', 'uvicorn'), + ('starlette', 'starlette'), + ('pydantic', 'pydantic'), + ('zeroconf', 'zeroconf'), + ('zeroconf._services', 'zeroconf/_services'), + ('PIL', 'PIL'), + ('PIL.Image', 'PIL'), + ('yaml', 'yaml'), ] + +tested = 0 +skipped = 0 failed = [] -for mod in modules: +for mod, path in candidates: + if not os.path.exists(os.path.join(sp_dir, path)): + skipped += 1 + continue try: - __import__(mod) + importlib.import_module(mod) + tested += 1 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 - } + +print(f' Smoke test passed ({tested} imported, {skipped} not installed)') +PYEOF }