feat(esp32): scope ESP-IDF resolution to the project's SAVED library manifest

Adds the get_project_libraries hook: the compile route reads a saved project's
declared library manifest (by project_id) and uses it as the ESP-IDF resolution
scope, preferring it over the client-sent manifest. So a saved project always
compiles against only its own declared libraries — never another user's, or
another project's, stray install in the shared dir — authoritatively from the
server, independent of frontend wiring. Client-sent manifest still used for
unsaved examples; None/empty → legacy scan-all. Overlay fills the hook in
register_pro; OSS default is no-op (None).
This commit is contained in:
David Montero 2026-06-07 02:01:08 +02:00
parent 4a21c4f938
commit b7954fa8c5
2 changed files with 43 additions and 2 deletions

View File

@ -8,7 +8,7 @@ from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel
from app.core.hooks import get_current_user_id, record_compile
from app.core.hooks import get_current_user_id, get_project_libraries, record_compile
from app.services.arduino_cli import ArduinoCLIService
from app.services.espidf_compiler import espidf_compiler
@ -215,12 +215,23 @@ async def _run_compile(
[f.model_dump() for f in request.spiffs_files]
if request.spiffs_files else None
)
# Library manifest = ESP-IDF resolution SCOPE. Prefer the manifest SAVED
# with the project (authoritative, read server-side from the project
# record) so a reloaded project always scopes to its own libraries
# regardless of what the client sends; fall back to the manifest the
# client sent (unsaved examples). None/empty → legacy scan-all.
allowed_libraries = None
project_libs = await get_project_libraries(request.project_id)
if project_libs:
allowed_libraries = set(project_libs)
elif request.libraries:
allowed_libraries = set(request.libraries)
result = await espidf_compiler.compile(
files, request.board_fqbn,
progress_callback=progress_callback,
board_options=request.board_options,
spiffs_files=spiffs_dicts,
allowed_libraries=set(request.libraries) if request.libraries is not None else None,
allowed_libraries=allowed_libraries,
)
return CompileResponse(
success=result["success"],

View File

@ -103,6 +103,36 @@ async def get_current_user_id(request: Request) -> Optional[str]: # FastAPI dep
return None
# ── get_project_libraries ─────────────────────────────────────────────────────
# Returns the declared library manifest (list of library names) SAVED with a
# project. The ESP-IDF compiler uses it as the resolution SCOPE so a project
# only merges its own declared libraries — never another user's, or another
# project's, stray install in the shared library dir. Sourced authoritatively
# from the project record (not the client), so it is robust regardless of what
# the frontend sends. Returns None for an unknown project, an empty manifest,
# or when no overlay is loaded (→ legacy scan-all).
GetProjectLibrariesHook = Callable[[str], Awaitable[Optional[list[str]]]]
_get_project_libraries_hook: Optional[GetProjectLibrariesHook] = None
def register_get_project_libraries(hook: GetProjectLibrariesHook) -> None:
"""Install the project-manifest resolver. Called by overlays in register_pro."""
global _get_project_libraries_hook
_get_project_libraries_hook = hook
async def get_project_libraries(project_id: Optional[str]) -> Optional[list[str]]:
if _get_project_libraries_hook is None or not project_id:
return None
try:
return await _get_project_libraries_hook(project_id)
except Exception:
logger.exception("get_project_libraries hook failed (treating as no manifest)")
return None
# ── lifespan startup ──────────────────────────────────────────────────────────
# Overlays that need to run async setup during FastAPI lifespan (DB init,
# table creation, legacy column migrations, etc.) register a coroutine here.