diff --git a/backend/app/api/routes/compile.py b/backend/app/api/routes/compile.py index e652d669..20c5bf7b 100644 --- a/backend/app/api/routes/compile.py +++ b/backend/app/api/routes/compile.py @@ -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"], diff --git a/backend/app/core/hooks.py b/backend/app/core/hooks.py index 0034537f..d7b6b68a 100644 --- a/backend/app/core/hooks.py +++ b/backend/app/core/hooks.py @@ -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.