diff --git a/backend/app/api/routes/compile.py b/backend/app/api/routes/compile.py index 996a5dab..7f236e8d 100644 --- a/backend/app/api/routes/compile.py +++ b/backend/app/api/routes/compile.py @@ -8,7 +8,12 @@ from typing import Any from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel -from app.core.hooks import get_current_user_id, get_project_libraries, record_compile +from app.core.hooks import ( + get_current_user_id, + get_project_libraries, + get_project_owner, + record_compile, +) from app.services.arduino_cli import ArduinoCLIService from app.services.espidf_compiler import espidf_compiler @@ -229,12 +234,17 @@ async def _run_compile( project_libs = await get_project_libraries(request.project_id) if project_libs: allowed_libraries = set(project_libs) + # P2.2 — the OWNER of the project (not the requester) owns any per-user + # custom libraries the manifest references, so resolve the owner so the + # scope materializer can find them. None for unsaved/anon compiles. + owner_id = await get_project_owner(request.project_id) result = await espidf_compiler.compile( files, request.board_fqbn, progress_callback=progress_callback, board_options=request.board_options, spiffs_files=spiffs_dicts, allowed_libraries=allowed_libraries, + owner_id=owner_id, ) return CompileResponse( success=result["success"], diff --git a/backend/app/core/hooks.py b/backend/app/core/hooks.py index 64aee787..41c6d8b1 100644 --- a/backend/app/core/hooks.py +++ b/backend/app/core/hooks.py @@ -145,7 +145,11 @@ async def get_project_libraries(project_id: Optional[str]) -> Optional[list[str] # empty -> the compiler uses its single default libraries dir (OSS self-host # parity / scan-all). SYNC — pure filesystem (symlink creation). -MaterializeLibraryScopeHook = Callable[[set], Optional[tuple]] +# `owner_id` is the project OWNER's id (NOT the requester's): a shared / embed / +# anonymous compile of someone else's project must resolve THAT user's custom +# (per-user-store) libraries. The overlay treats it as an opaque key; the OSS +# compiler only threads it through. None for unsaved/anon-no-project compiles. +MaterializeLibraryScopeHook = Callable[[set, Optional[str]], Optional[tuple]] _materialize_library_scope_hook: Optional[MaterializeLibraryScopeHook] = None @@ -156,19 +160,48 @@ def register_materialize_library_scope(hook: MaterializeLibraryScopeHook) -> Non _materialize_library_scope_hook = hook -def materialize_library_scope(allowed_libraries: Optional[set]) -> Optional[tuple]: +def materialize_library_scope( + allowed_libraries: Optional[set], owner_id: Optional[str] = None +) -> Optional[tuple]: """Return (libraries_dir, content_token) for the manifest, or None to use the compiler's default single libraries dir. Never raises (a failing materializer degrades to the default dir).""" if _materialize_library_scope_hook is None or not allowed_libraries: return None try: - return _materialize_library_scope_hook(allowed_libraries) + return _materialize_library_scope_hook(allowed_libraries, owner_id) except Exception: logger.exception("materialize_library_scope hook failed (using default libraries dir)") return None +# ── get_project_owner ───────────────────────────────────────────────────────── +# Resolve a project's OWNER user-id from its id (the value stored as user_id on +# the project record). Used so a compile resolves the OWNER's per-user custom +# libraries, not the requester's. Returns None for an unknown project or when no +# overlay is loaded. + +GetProjectOwnerHook = Callable[[str], Awaitable[Optional[str]]] + +_get_project_owner_hook: Optional[GetProjectOwnerHook] = None + + +def register_get_project_owner(hook: GetProjectOwnerHook) -> None: + """Install the project-owner resolver. Called by overlays in register_pro.""" + global _get_project_owner_hook + _get_project_owner_hook = hook + + +async def get_project_owner(project_id: Optional[str]) -> Optional[str]: + if _get_project_owner_hook is None or not project_id: + return None + try: + return await _get_project_owner_hook(project_id) + except Exception: + logger.exception("get_project_owner hook failed (treating as no owner)") + 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. diff --git a/backend/app/services/espidf_compiler.py b/backend/app/services/espidf_compiler.py index 9b8bc5ee..3be72ace 100644 --- a/backend/app/services/espidf_compiler.py +++ b/backend/app/services/espidf_compiler.py @@ -1718,6 +1718,7 @@ class ESPIDFCompiler: board_options: dict | None = None, spiffs_files: list[dict] | None = None, allowed_libraries: set[str] | None = None, + owner_id: str | None = None, ) -> dict: """ Compile Arduino sketch using ESP-IDF. @@ -1804,7 +1805,7 @@ class ESPIDFCompiler: # cache update) gets its own clean build dir — and the throwaway # scope dir is removed after the attempt (its files were already # copied into the build's user_libs_all by _compile_in_dir). - scope = materialize_library_scope(allowed) + scope = materialize_library_scope(allowed, owner_id) scope_dir = scope[0] if scope else None scope_token = scope[1] if scope else '' # Fold the effective library set + resolved content into the build-dir