fix: improve build_frontend reliability and cross-platform compatibility

Addresses concerns from PR #76 code review:

- Add exception handling for stat() calls to prevent crashes from race
  conditions when files are deleted/modified during iteration
- Add 2-second timestamp tolerance for FAT32 filesystem compatibility
  (FAT32 has 2-second mtime precision on USB drives/SD cards)
- Add config file checks (package.json, vite.config.ts, tailwind.config.ts,
  tsconfig.json, etc.) that also require rebuilds when changed
- Add logging to show which file triggered the rebuild for debugging

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Auto
2026-01-19 10:26:01 +02:00
parent 3c80611b59
commit fbe4c399ac

View File

@@ -141,38 +141,90 @@ def install_npm_deps() -> bool:
def build_frontend() -> bool:
"""Build the React frontend if dist doesn't exist or is stale."""
"""Build the React frontend if dist doesn't exist or is stale.
Staleness is determined by comparing modification times of:
- Source files in ui/src/
- Config files (package.json, vite.config.ts, etc.)
Against the newest file in ui/dist/
Includes a 2-second tolerance for FAT32 filesystem compatibility.
"""
dist_dir = UI_DIR / "dist"
src_dir = UI_DIR / "src"
# FAT32 has 2-second timestamp precision, so we add tolerance to avoid
# false negatives when projects are on USB drives or SD cards
TIMESTAMP_TOLERANCE = 2
# Config files that should trigger a rebuild when changed
CONFIG_FILES = [
"package.json",
"package-lock.json",
"vite.config.ts",
"tailwind.config.ts",
"tsconfig.json",
"tsconfig.node.json",
"postcss.config.js",
"index.html",
]
# Check if build is needed
needs_build = False
trigger_file = None
if not dist_dir.exists():
needs_build = True
trigger_file = "dist/ directory missing"
elif src_dir.exists():
# Find the newest file in dist/ directory
newest_dist_mtime = 0
for dist_file in dist_dir.rglob("*"):
if dist_file.is_file():
file_mtime = dist_file.stat().st_mtime
if file_mtime > newest_dist_mtime:
newest_dist_mtime = file_mtime
try:
if dist_file.is_file():
file_mtime = dist_file.stat().st_mtime
if file_mtime > newest_dist_mtime:
newest_dist_mtime = file_mtime
except (FileNotFoundError, PermissionError, OSError):
# File was deleted or became inaccessible during iteration
continue
# Check if any source file is newer than the newest dist file
if newest_dist_mtime > 0:
for src_file in src_dir.rglob("*"):
if src_file.is_file() and src_file.stat().st_mtime > newest_dist_mtime:
needs_build = True
break
# Check config files first (these always require rebuild)
for config_name in CONFIG_FILES:
config_path = UI_DIR / config_name
try:
if config_path.exists():
if config_path.stat().st_mtime > newest_dist_mtime + TIMESTAMP_TOLERANCE:
needs_build = True
trigger_file = config_name
break
except (FileNotFoundError, PermissionError, OSError):
continue
# Check source files if no config triggered rebuild
if not needs_build:
for src_file in src_dir.rglob("*"):
try:
if src_file.is_file():
if src_file.stat().st_mtime > newest_dist_mtime + TIMESTAMP_TOLERANCE:
needs_build = True
trigger_file = str(src_file.relative_to(UI_DIR))
break
except (FileNotFoundError, PermissionError, OSError):
# File was deleted or became inaccessible during iteration
continue
else:
# No files found in dist, need to rebuild
needs_build = True
trigger_file = "dist/ directory is empty"
if not needs_build:
print(" Frontend already built (up to date)")
return True
if trigger_file:
print(f" Rebuild triggered by: {trigger_file}")
print(" Building React frontend...")
npm_cmd = "npm.cmd" if sys.platform == "win32" else "npm"
return run_command([npm_cmd, "run", "build"], cwd=UI_DIR)