fix(build): keep .py sources + make smoke test skip uninstalled modules
Lint & Test / test (push) Successful in 1m36s

- compile_and_strip_sources: stop deleting .py files after compileall.
  OpenCV's loader does literal file I/O on cv2/config.py (not a Python
  import), so stripping it breaks `import cv2` with "missing
  configuration file: ['config.py']". Other packages may do similar
  file-based introspection tricks — the ~30% size win isn't worth
  playing whack-a-mole with broken installers. We already hit this
  with numpy.linalg and zeroconf._services; enough incidents.
- smoke_test_imports: only assert importability for modules whose
  top-level dir actually exists in site-packages. Pillow for example
  is a Windows-only dep, and was failing the Linux build spuriously.
  Rewrote as a heredoc for readability.
This commit is contained in:
2026-04-07 23:37:54 +03:00
parent fd6776aeac
commit 17c5c02993
+58 -29
View File
@@ -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
}