fix(build): keep .py sources + make smoke test skip uninstalled modules
Lint & Test / test (push) Successful in 1m36s
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:
+58
-29
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user