Files
pyxis/sync_file_libdeps.py
T

99 lines
4.0 KiB
Python

"""
PlatformIO pre-build: mirror file:// lib_deps from their source trees
into .pio/libdeps before each build.
PlatformIO copies a `file://...` lib_dep into `.pio/libdeps/<env>/<lib>`
once on first install and never re-syncs even when the source changes.
That silently masks fixes — `pio run` succeeds and the device flashes,
but the firmware contains the OLD version of the source. Bit us hard
on a microReticulum security fix that lived in source for a full test
session before anyone noticed it hadn't deployed.
For each registered file:// dep below, this script rsyncs the source
tree into the libdep cache, preserving timestamps so the linker sees
fresh source on every build.
"""
Import("env")
import os
import sys
import shutil
import filecmp
from pathlib import Path
sys.path.insert(0, env.get("PROJECT_DIR", "."))
from _build_helpers import env_libdeps_dir # per-env libdeps path; never hardcode the env
# (source_dir, libdep_subdir_name) — populated from env vars so the
# script doesn't bake any contributor's local checkout path into the
# committed source. Since microReticulum is now consumed as a pinned
# git URL in platformio.ini (not a file:// dep), this script no-ops
# for everyone by default. Set PYXIS_MICRORETICULUM_DIR to opt in to
# a local-override workflow (mirrors that source tree into
# .pio/libdeps/<env>/microReticulum on each build).
FILE_DEPS = []
_micro_reticulum_dir = os.environ.get("PYXIS_MICRORETICULUM_DIR", "").strip()
if _micro_reticulum_dir:
FILE_DEPS.append((_micro_reticulum_dir, "microReticulum"))
PROJECT_DIR = Path(env.get("PROJECT_DIR", "."))
LIBDEPS_BASE = Path(env_libdeps_dir(env))
def mirror(src: Path, dst: Path):
if not src.exists():
print(f"SYNC: {src} missing, skipping")
return False
if not dst.exists():
# Let PIO do the first install; we only refresh after that.
print(f"SYNC: {dst} not yet installed, skipping (PIO will fetch)")
return False
src_files = 0
copied = 0
removed = 0
mirrored_paths = set()
for root, dirs, files in os.walk(src):
# Skip git, build artifacts.
dirs[:] = [d for d in dirs if d not in (".git", ".pio", "__pycache__", "build")]
rel = Path(root).relative_to(src)
for fn in files:
if fn.endswith((".pyc", ".o", ".a", ".elf", ".bin")):
continue
sp = Path(root) / fn
dp = dst / rel / fn
mirrored_paths.add((rel / fn).as_posix())
src_files += 1
# A freshly fetched dependency can have a newer mtime than an older
# local checkout even when the local bytes contain the intended fix.
# Compare content so the explicit local override always wins.
if not dp.exists() or not filecmp.cmp(sp, dp, shallow=False):
dp.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(sp, dp)
# copy2 preserves the source mtime. Touch the mirrored file so
# incremental PlatformIO builds recompile changed dependency code.
dp.touch()
copied += 1
# A mirror must also remove source files deleted from the explicit local
# checkout. Otherwise PlatformIO can continue compiling stale translation
# units that no longer exist in the dependency being tested.
for root, dirs, files in os.walk(dst):
dirs[:] = [d for d in dirs if d not in (".git", ".pio", "__pycache__", "build")]
rel = Path(root).relative_to(dst)
for fn in files:
if fn == ".piopm" or fn.endswith((".pyc", ".o", ".a", ".elf", ".bin")):
continue
relative_path = (rel / fn).as_posix()
if relative_path not in mirrored_paths:
(Path(root) / fn).unlink()
removed += 1
if copied > 0 or removed > 0:
print(
f"SYNC: {dst.name}: refreshed {copied}/{src_files} files, "
f"removed {removed} stale files from {src}"
)
return True
for src, name in FILE_DEPS:
mirror(Path(src), LIBDEPS_BASE / name)