diff --git a/build-common.sh b/build-common.sh index 7d331aa..e25cfec 100644 --- a/build-common.sh +++ b/build-common.sh @@ -24,6 +24,14 @@ detect_version() { VERSION_CLEAN="${version#v}" + # Normalize non-PEP440 version labels (e.g. "dev", "nightly", "snapshot") + # to a valid PEP440 dev release. Without this, pip/setuptools rejects the + # pyproject.toml with: `project.version` must be pep440. + if ! [[ "$VERSION_CLEAN" =~ ^[0-9]+(\.[0-9]+)*((a|b|rc|\.dev|\.post)[0-9]+)*(\+[a-zA-Z0-9.]+)?$ ]]; then + echo " Warning: '$VERSION_CLEAN' is not PEP440-compliant, using 0.0.0.dev0" + VERSION_CLEAN="0.0.0.dev0" + fi + # Stamp the resolved version into pyproject.toml so that # importlib.metadata reads the correct value at runtime. sed -i "s/^version = .*/version = \"${VERSION_CLEAN}\"/" "$SERVER_DIR/pyproject.toml" @@ -127,10 +135,6 @@ cleanup_site_packages() { find "$sp_dir" -name "*.$ext_suffix" -exec strip --strip-debug {} \; 2>/dev/null || true fi - # ── Remove .py source (keep .pyc bytecode) ─────────────── - echo " Removing .py source from site-packages (keeping .pyc)..." - find "$sp_dir" -name "*.py" ! -name "__init__.py" -delete 2>/dev/null || true - # ── Remove wled_controller if pip-installed ─────────────── rm -rf "$sp_dir"/wled_controller* "$sp_dir"/wled*.dist-info 2>/dev/null || true @@ -138,3 +142,37 @@ cleanup_site_packages() { cleaned_size=$(du -sh "$sp_dir" | cut -f1) echo " Site-packages after cleanup: $cleaned_size" } + +# ── Pre-compile .py → .pyc and strip 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. +# +# Args: +# $1 — directory to compile (site-packages or app/src) +# $2 — python executable to use (default: python3) + +compile_and_strip_sources() { + local target_dir="$1" + local py_cmd="${2:-python3}" + + if [ ! -d "$target_dir" ]; then + return 0 + fi + + echo " Pre-compiling Python bytecode in $(basename "$target_dir")..." + "$py_cmd" -m compileall -b -q "$target_dir" 2>/dev/null || { + 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 + find "$target_dir" -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true +} diff --git a/build-dist-windows.sh b/build-dist-windows.sh index 54a8186..a6f83e5 100644 --- a/build-dist-windows.sh +++ b/build-dist-windows.sh @@ -273,6 +273,12 @@ rm -rf "$SITE_PACKAGES"/pythonwin 2>/dev/null || true rm -f "$SITE_PACKAGES"/PyWin32.chm 2>/dev/null || true find "$SITE_PACKAGES/winrt" -name "*.pyi" -delete 2>/dev/null || true +# Pre-compile and strip .py sources. MUST run AFTER cleanup (so we don't +# waste work compiling files about to be deleted). Uses host python — +# PYTHON_VERSION above must match the embedded Python major.minor or +# the generated .pyc will ImportError on the target. +compile_and_strip_sources "$SITE_PACKAGES" "python" + WHEEL_COUNT=$(ls "$WHEEL_DIR"/*.whl 2>/dev/null | wc -l) echo " Installed $WHEEL_COUNT packages" @@ -286,10 +292,9 @@ build_frontend echo "[8/9] Copying application files..." copy_app_files -# Pre-compile Python bytecode for faster startup -echo " Pre-compiling Python bytecode..." +# Pre-compile app source for faster startup (keep .py too — app source +# is small and easier to debug in-place if a user reports an issue) python -m compileall -b -q "$APP_DIR/src" 2>/dev/null || true -python -m compileall -b -q "$SITE_PACKAGES" 2>/dev/null || true # ── Create launcher ────────────────────────────────────────── diff --git a/build-dist.sh b/build-dist.sh index af00ca7..7e77a80 100644 --- a/build-dist.sh +++ b/build-dist.sh @@ -53,6 +53,9 @@ SITE_PACKAGES=$(echo "$VENV_DIR"/lib/python*/site-packages) # Clean up with shared function cleanup_site_packages "$SITE_PACKAGES" "so" "so" +# Pre-compile and strip .py sources (must happen AFTER cleanup) +compile_and_strip_sources "$SITE_PACKAGES" "python" + # ── Build frontend ─────────────────────────────────────────── echo "[4/7] Building frontend..." @@ -63,6 +66,10 @@ build_frontend echo "[5/7] Copying application files..." copy_app_files +# Pre-compile app source for faster startup (keep .py too — app source +# is small and easier to debug in-place if a user reports an issue) +python -m compileall -b -q "$APP_DIR/src" 2>/dev/null || true + # ── Create launcher ────────────────────────────────────────── echo "[6/7] Creating launcher..." diff --git a/installer.nsi b/installer.nsi index faeb8f5..e6f777c 100644 --- a/installer.nsi +++ b/installer.nsi @@ -87,6 +87,18 @@ Section "!${APPNAME} (required)" SecCore SetOutPath "$INSTDIR" + ; Wipe prior payload dirs before extracting. NSIS File /r MERGES files + ; on top of existing ones — on an upgrade, stale .pyc/.pyd from the old + ; version (and any files removed or renamed since) would survive, + ; producing a half-old/half-new install that presents as "version + ; mismatch" or "duplicate package" ImportErrors at runtime. + ; IMPORTANT: only touch payload dirs — never $INSTDIR\data or + ; $INSTDIR\logs (user config must be preserved across upgrades). + RMDir /r "$INSTDIR\python" + RMDir /r "$INSTDIR\app" + RMDir /r "$INSTDIR\scripts" + Delete "$INSTDIR\LedGrab.bat" + ; Copy the entire portable build File /r "build\LedGrab\python" File /r "build\LedGrab\app"