diff --git a/backend/app/api/frames.py b/backend/app/api/frames.py index 3fd072b2d..3e2cef706 100644 --- a/backend/app/api/frames.py +++ b/backend/app/api/frames.py @@ -104,7 +104,7 @@ from app.utils.tls import generate_frame_tls_material, parse_certificate_not_valid_after from app.utils.ssh_authorized_keys import _install_authorized_keys, resolve_authorized_keys_update from app.tasks.binary_builder import FrameBinaryBuilder -from app.codegen.drivers_nim import frame_driver_build_mode +from app.codegen.drivers_nim import frame_compilation_mode from app.utils.local_exec import exec_local_command from app.utils.jwt_tokens import validate_scoped_token from . import api_with_auth, api_no_auth @@ -770,7 +770,7 @@ async def api_frame_local_build_zip( # noqa: D401 source_dir = deployer.create_local_source_folder(tmp) # Apply all frame‑specific code generation (scenes, drivers, …) - await deployer.make_local_modifications(source_dir, driver_build_mode=frame_driver_build_mode(frame)) + await deployer.make_local_modifications(source_dir, compilation_mode=frame_compilation_mode(frame)) await copy_custom_fonts_to_local_source_folder(db, source_dir) # Package → .zip @@ -833,13 +833,13 @@ async def api_frame_local_c_source_zip( ) source_dir = deployer.create_local_source_folder(tmp) - driver_build_mode = frame_driver_build_mode(frame) - await deployer.make_local_modifications(source_dir, driver_build_mode=driver_build_mode) + compilation_mode = frame_compilation_mode(frame) + await deployer.make_local_modifications(source_dir, compilation_mode=compilation_mode) await copy_custom_fonts_to_local_source_folder(db, source_dir) build_dir = os.path.join(tmp, f"build_{deployer.build_id}") os.makedirs(build_dir, exist_ok=True) - await deployer.create_local_build_archive(build_dir, source_dir, arch, driver_build_mode=driver_build_mode) + await deployer.create_local_build_archive(build_dir, source_dir, arch, compilation_mode=compilation_mode) zip_path = os.path.join(tmp, f"frameos_{deployer.build_id}_c_source.zip") with zipfile.ZipFile( @@ -922,6 +922,16 @@ async def api_frame_local_binary_zip( detail=f"Shared driver library missing after build: {driver_library_path}", ) shutil.copy2(driver_library_path, os.path.join(driver_dir, os.path.basename(driver_library_path))) + if build_result.scene_library_paths: + scene_dir = os.path.join(dist_dir, "scenes") + os.makedirs(scene_dir, exist_ok=True) + for scene_library_path in build_result.scene_library_paths: + if not os.path.isfile(scene_library_path): + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail=f"Shared scene library missing after build: {scene_library_path}", + ) + shutil.copy2(scene_library_path, os.path.join(scene_dir, os.path.basename(scene_library_path))) zip_path = os.path.join(tmp, f"frameos_{deployer.build_id}_binary.zip") with zipfile.ZipFile( diff --git a/backend/app/codegen/drivers_nim.py b/backend/app/codegen/drivers_nim.py index 7eda56f87..51fb3a035 100644 --- a/backend/app/codegen/drivers_nim.py +++ b/backend/app/codegen/drivers_nim.py @@ -5,33 +5,33 @@ from app.drivers.drivers import Driver -DRIVER_BUILD_MODE_STATIC = "static" -DRIVER_BUILD_MODE_SHARED = "shared" -DRIVER_BUILD_MODE_PRECOMPILED = "precompiled" -DEFAULT_DRIVER_BUILD_MODE = DRIVER_BUILD_MODE_STATIC -VALID_DRIVER_BUILD_MODES = { - DRIVER_BUILD_MODE_STATIC, - DRIVER_BUILD_MODE_SHARED, - DRIVER_BUILD_MODE_PRECOMPILED, +COMPILATION_MODE_STATIC = "static" +COMPILATION_MODE_SHARED = "shared" +COMPILATION_MODE_PRECOMPILED = "precompiled" +DEFAULT_COMPILATION_MODE = COMPILATION_MODE_STATIC +VALID_COMPILATION_MODES = { + COMPILATION_MODE_STATIC, + COMPILATION_MODE_SHARED, + COMPILATION_MODE_PRECOMPILED, } -def normalize_driver_build_mode(value: str | None) -> str: - normalized = (value or DEFAULT_DRIVER_BUILD_MODE).strip().lower() - if normalized not in VALID_DRIVER_BUILD_MODES: - return DEFAULT_DRIVER_BUILD_MODE +def normalize_compilation_mode(value: str | None) -> str: + normalized = (value or DEFAULT_COMPILATION_MODE).strip().lower() + if normalized not in VALID_COMPILATION_MODES: + return DEFAULT_COMPILATION_MODE return normalized -def frame_driver_build_mode(frame) -> str: +def frame_compilation_mode(frame) -> str: rpios_settings = getattr(frame, "rpios", None) or {} - return normalize_driver_build_mode(rpios_settings.get("driverBuildMode")) + return normalize_compilation_mode(rpios_settings.get("compilationMode")) -def driver_build_mode_uses_shared_libraries(value: str | None) -> bool: - return normalize_driver_build_mode(value) in { - DRIVER_BUILD_MODE_SHARED, - DRIVER_BUILD_MODE_PRECOMPILED, +def compilation_mode_uses_shared_libraries(value: str | None) -> bool: + return normalize_compilation_mode(value) in { + COMPILATION_MODE_SHARED, + COMPILATION_MODE_PRECOMPILED, } @@ -241,9 +241,9 @@ def driver_library_context_helpers_nim() -> str: def write_drivers_nim( drivers: dict[str, Driver], - driver_build_mode: str = DEFAULT_DRIVER_BUILD_MODE, + compilation_mode: str = DEFAULT_COMPILATION_MODE, ) -> str: - if driver_build_mode_uses_shared_libraries(driver_build_mode): + if compilation_mode_uses_shared_libraries(compilation_mode): return write_shared_drivers_nim(drivers) return write_static_drivers_nim(drivers) diff --git a/backend/app/codegen/scene_nim.py b/backend/app/codegen/scene_nim.py index 460ea9fca..df1ad77fd 100644 --- a/backend/app/codegen/scene_nim.py +++ b/backend/app/codegen/scene_nim.py @@ -7,6 +7,10 @@ from app.models.frame import Frame from app.models.apps import get_local_frame_apps, get_local_app_path, get_scene_app_id +from app.codegen.drivers_nim import ( + DEFAULT_COMPILATION_MODE, + compilation_mode_uses_shared_libraries, +) from app.codegen.utils import sanitize_nim_string, natural_keys from app.utils.js_apps import find_js_app_source_key @@ -36,6 +40,50 @@ def write_scene_nim(frame: Frame, scene: dict) -> str: return SceneWriter(frame, scene).write_scene_nim() +def compiled_frame_scenes(frame: Frame) -> list[dict]: + scenes = getattr(frame, "scenes", None) or [] + compiled_scenes = [] + for scene in scenes: + if not isinstance(scene, dict): + continue + settings = scene.get("settings") or {} + if settings.get("execution", "compiled") != "interpreted": + compiled_scenes.append(scene) + return compiled_scenes + + +def scene_registry_id(scene: dict) -> str: + return re.sub(r"[^a-zA-Z0-9\-\_]", "_", scene.get("id", "default")) + + +def scene_module_suffix(scene: dict) -> str: + return re.sub(r"\W+", "", scene_registry_id(scene)) + + +def scene_module_filename(scene: dict) -> str: + return f"scene_{scene_module_suffix(scene)}.nim" + + +def scene_library_filename(scene: dict) -> str: + return f"scene_{scene_module_suffix(scene)}.so" + + +def write_scene_library_nim(scene: dict) -> str: + scene_module = f"scene_{scene_module_suffix(scene)}" + return f"""# This file is autogenerated + +import scenes/{scene_module} as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {{.cdecl, exportc, dynlib.}} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {{.cdecl, exportc, dynlib.}} = + result = cast[pointer](sceneModule.exportedScene) +""" + + def field_type_to_nim_type(field_type: str, required: bool = True) -> str: match field_type: case 'select': @@ -1633,47 +1681,46 @@ def wrap_with_cache(self, node_id: str, value_list: list[str], data: dict): return value_list -def write_scenes_nim(frame: Frame) -> str: +def _scene_registry_rows(frame: Frame) -> tuple[list[str], list[str], dict | None]: rows = [] - imports = [] sceneOptionTuples = [] default_scene = None - for scene in frame.scenes: - execution = scene.get("settings", {}).get("execution", "compiled") - if execution == "interpreted": - continue + for scene in compiled_frame_scenes(frame): if scene.get("default", False): default_scene = scene - scene_id = scene.get("id", "default") - scene_id = re.sub(r"[^a-zA-Z0-9\-\_]", "_", scene_id) - scene_id_import = re.sub(r"\W+", "", scene_id) - imports.append( - f"import scenes/scene_{scene_id_import} as scene_{scene_id_import}" - ) + scene_id = scene_registry_id(scene) rows.append( - f' result["{scene_id}".SceneId] = scene_{scene_id_import}.exportedScene' + f' result["{scene_id}".SceneId] = scene_{scene_module_suffix(scene)}.exportedScene' ) sceneOptionTuples.append( - f" (\"{scene_id}\".SceneId, \"{scene.get('name', 'Default')}\")," + f' ("{scene_id}".SceneId, "{sanitize_nim_string(scene.get("name", "Default"))}"),' ) + return rows, sceneOptionTuples, default_scene + +def _default_scene_line(default_scene: dict | None) -> str: default_scene_id = ( default_scene.get("id", None) if default_scene is not None else None ) if default_scene_id is None: - default_scene_line = "let defaultSceneId* = none(SceneId)" - else: - default_scene_id = re.sub(r"[^a-zA-Z0-9\-\_]", "_", default_scene_id) - default_scene_line = f'let defaultSceneId* = some("{default_scene_id}".SceneId)' + return "let defaultSceneId* = none(SceneId)" + return f'let defaultSceneId* = some("{scene_registry_id(default_scene)}".SceneId)' + +def write_static_scenes_nim(frame: Frame) -> str: + rows, sceneOptionTuples, default_scene = _scene_registry_rows(frame) + imports = [ + f"import scenes/scene_{scene_module_suffix(scene)} as scene_{scene_module_suffix(scene)}" + for scene in compiled_frame_scenes(frame) + ] newline = "\n" scenes_source = f""" import frameos/types import tables, options {newline.join(sorted(imports))} -{default_scene_line} +{_default_scene_line(default_scene)} const sceneOptions*: array[{len(sceneOptionTuples)}, tuple[id: SceneId, name: string]] = [ {newline.join(sorted(sceneOptionTuples))} @@ -1685,3 +1732,117 @@ def write_scenes_nim(frame: Frame) -> str: """ return scenes_source + + +def write_shared_scenes_nim(frame: Frame) -> str: + compiled_scenes = compiled_frame_scenes(frame) + sceneOptionTuples = [ + f' ("{scene_registry_id(scene)}".SceneId, "{sanitize_nim_string(scene.get("name", "Default"))}"),' + for scene in compiled_scenes + ] + default_scene = next((scene for scene in compiled_scenes if scene.get("default", False)), None) + specs = [ + "SceneSpec(" + f'id: "{scene_registry_id(scene)}".SceneId, ' + f'name: "{sanitize_nim_string(scene.get("name", "Default"))}", ' + f'libraryName: "{scene_library_filename(scene)}"' + ")" + for scene in compiled_scenes + ] + + newline = "\n" + spec_lines = ("," + newline + " ").join(specs) + if spec_lines: + spec_lines = newline + " " + spec_lines + newline + + scenes_source = f""" +import std/[dynlib, json, options, os, tables] +import frameos/types +import frameos/channels as hostChannels +import frameos/driver_abi + +type + SceneSpec = object + id: SceneId + name: string + libraryName: string + + LoadedSceneLibrary = object + spec: SceneSpec + library: LibHandle + exportedScene: ExportedScene + + SceneInitProc = proc(logHook: HostLogProc, sendEventHook: HostSendEventProc) {{.cdecl.}} + SceneExportProc = proc(): pointer {{.cdecl.}} + +let sceneSpecs: seq[SceneSpec] = @[{spec_lines}] +var loadedSceneLibraries: seq[LoadedSceneLibrary] = @[] + +{_default_scene_line(default_scene)} + +const sceneOptions*: array[{len(sceneOptionTuples)}, tuple[id: SceneId, name: string]] = [ +{newline.join(sorted(sceneOptionTuples))} +] + +proc hostLog(event: JsonNode) {{.cdecl, gcsafe.}} = + hostChannels.log(event) + +proc hostSendEvent(scene: Option[SceneId], event: string, payload: JsonNode) {{.cdecl, gcsafe.}} = + hostChannels.sendEvent(scene, event, payload) + +proc sceneLibraryPath(spec: SceneSpec): string = + getAppDir() / "scenes" / spec.libraryName + +proc loadRequiredSymbol[T](library: LibHandle, sceneId: SceneId, symbol: string): T = + let address = symAddr(library, symbol) + if address.isNil: + hostChannels.log(%*{{"event": "scene:shared:error", "sceneId": sceneId.string, + "error": "Missing symbol", "symbol": symbol}}) + return nil + cast[T](address) + +proc loadSharedScene(spec: SceneSpec): Option[ExportedScene] = + let path = sceneLibraryPath(spec) + let library = loadLib(path) + if library.isNil: + hostChannels.log(%*{{"event": "scene:shared:error", "sceneId": spec.id.string, + "error": "Unable to load scene library", "path": path}}) + return none(ExportedScene) + + let initProc = loadRequiredSymbol[SceneInitProc](library, spec.id, "frameos_scene_init") + if initProc.isNil: + unloadLib(library) + return none(ExportedScene) + initProc(hostLog, hostSendEvent) + + let exportProc = loadRequiredSymbol[SceneExportProc](library, spec.id, "frameos_scene_export") + if exportProc.isNil: + unloadLib(library) + return none(ExportedScene) + + let exportedScene = cast[ExportedScene](exportProc()) + if exportedScene.isNil: + hostChannels.log(%*{{"event": "scene:shared:error", "sceneId": spec.id.string, + "error": "Scene library returned nil export", "path": path}}) + unloadLib(library) + return none(ExportedScene) + + loadedSceneLibraries.add(LoadedSceneLibrary(spec: spec, library: library, exportedScene: exportedScene)) + hostChannels.log(%*{{"event": "scene:shared", "sceneId": spec.id.string, "path": path, "loaded": true}}) + return some(exportedScene) + +proc getExportedScenes*(): Table[SceneId, ExportedScene] = + result = initTable[SceneId, ExportedScene]() + for spec in sceneSpecs: + let exportedScene = loadSharedScene(spec) + if exportedScene.isSome: + result[spec.id] = exportedScene.get() +""" + + return scenes_source + + +def write_scenes_nim(frame: Frame, compilation_mode: str = DEFAULT_COMPILATION_MODE) -> str: + if compilation_mode_uses_shared_libraries(compilation_mode): + return write_shared_scenes_nim(frame) + return write_static_scenes_nim(frame) diff --git a/backend/app/codegen/tests/test_scene_nim.py b/backend/app/codegen/tests/test_scene_nim.py index ffe36de44..af03c3831 100644 --- a/backend/app/codegen/tests/test_scene_nim.py +++ b/backend/app/codegen/tests/test_scene_nim.py @@ -1,6 +1,6 @@ from types import SimpleNamespace -from app.codegen.scene_nim import write_scene_nim +from app.codegen.scene_nim import scene_library_filename, write_scene_library_nim, write_scene_nim, write_scenes_nim def test_app_output_field_input_is_coerced_to_target_field_type(): @@ -180,3 +180,41 @@ def test_native_app_output_field_input_keeps_native_return_type(): assert "self.node1.appConfig.text = block:\n self.node2.get(context)" in source assert "self.node2.get(context).asString()" not in source + + +def test_shared_scene_registry_loads_scene_libraries(): + frame = SimpleNamespace( + scenes=[ + { + "id": "my-scene", + "name": "My Scene", + "default": True, + "settings": {"execution": "compiled"}, + }, + { + "id": "live-scene", + "name": "Live Scene", + "settings": {"execution": "interpreted"}, + }, + ] + ) + + source = write_scenes_nim(frame, compilation_mode="shared") + + assert 'libraryName: "scene_myscene.so"' in source + assert '"my-scene".SceneId' in source + assert "loadLib(path)" in source + assert '"frameos_scene_init"' in source + assert '"frameos_scene_export"' in source + assert "scene_live_scene" not in source + + +def test_scene_library_wrapper_exports_scene_symbols(): + scene = {"id": "my-scene", "name": "My Scene"} + source = write_scene_library_nim(scene) + + assert scene_library_filename(scene) == "scene_myscene.so" + assert "import scenes/scene_myscene as sceneModule" in source + assert "proc frameos_scene_init*" in source + assert "setSharedHostCallbacks(logHook, sendEventHook)" in source + assert "proc frameos_scene_export*" in source diff --git a/backend/app/tasks/_frame_deployer.py b/backend/app/tasks/_frame_deployer.py index aacba7866..e1a5ce170 100644 --- a/backend/app/tasks/_frame_deployer.py +++ b/backend/app/tasks/_frame_deployer.py @@ -27,16 +27,24 @@ from app.drivers.devices import drivers_for_frame from app.models import get_apps_from_scenes from app.codegen.drivers_nim import ( - DEFAULT_DRIVER_BUILD_MODE, - DRIVER_BUILD_MODE_STATIC, + DEFAULT_COMPILATION_MODE, + COMPILATION_MODE_STATIC, compiled_drivers, - driver_build_mode_uses_shared_libraries, + compilation_mode_uses_shared_libraries, driver_library_filename, - normalize_driver_build_mode, + normalize_compilation_mode, write_driver_library_nim, write_drivers_nim, ) -from app.codegen.scene_nim import write_scene_nim, write_scenes_nim +from app.codegen.scene_nim import ( + compiled_frame_scenes, + scene_library_filename, + scene_module_filename, + scene_module_suffix, + write_scene_library_nim, + write_scene_nim, + write_scenes_nim, +) from app.tasks.utils import find_nimbase_file from app.codegen.apps_nim import write_apps_nim from app.codegen.app_loader_nim import write_app_loader_nim @@ -53,6 +61,10 @@ "--passL:-Wl,--gc-sections", ) +SHARED_LIBRARY_NIM_FLAGS = tuple( + flag for flag in DRIVER_LIBRARY_NIM_FLAGS if flag != "--define:frameosDriverLibrary" +) + DRIVER_LIBRARY_CFLAGS = ( "-ffunction-sections", "-fdata-sections", @@ -180,9 +192,9 @@ def _driver_linker_flags(drivers: dict[str, Driver]) -> list[str]: def driver_library_paths( build_dir: str, drivers: dict[str, Driver], - driver_build_mode: str, + compilation_mode: str, ) -> list[str]: - if not driver_build_mode_uses_shared_libraries(driver_build_mode): + if not compilation_mode_uses_shared_libraries(compilation_mode): return [] return [ os.path.join(build_dir, "drivers", driver.name, driver_library_filename(driver)) @@ -192,12 +204,34 @@ def driver_library_paths( @staticmethod def driver_library_names( drivers: dict[str, Driver], - driver_build_mode: str, + compilation_mode: str, ) -> list[str]: - if not driver_build_mode_uses_shared_libraries(driver_build_mode): + if not compilation_mode_uses_shared_libraries(compilation_mode): return [] return [driver_library_filename(driver) for driver in compiled_drivers(drivers)] + @staticmethod + def scene_library_paths( + build_dir: str, + frame: Frame, + compilation_mode: str, + ) -> list[str]: + if not compilation_mode_uses_shared_libraries(compilation_mode): + return [] + return [ + os.path.join(build_dir, "scenes", scene_module_suffix(scene), scene_library_filename(scene)) + for scene in compiled_frame_scenes(frame) + ] + + @staticmethod + def scene_library_names( + frame: Frame, + compilation_mode: str, + ) -> list[str]: + if not compilation_mode_uses_shared_libraries(compilation_mode): + return [] + return [scene_library_filename(scene) for scene in compiled_frame_scenes(frame)] + async def _upload_frame_json(self, path: str) -> None: """Upload the release-specific `frame.json`.""" json_data = json.dumps(get_frame_json(self.db, self.frame), indent=4).encode() + b"\n" @@ -320,12 +354,12 @@ async def disable_service(self, service_name: str) -> None: async def make_local_modifications( self, source_dir: str, - driver_build_mode: str = DEFAULT_DRIVER_BUILD_MODE, + compilation_mode: str = DEFAULT_COMPILATION_MODE, drivers_override: dict[str, Driver] | None = None, drivers_nim_source: str | None = None, ): frame = self.frame - driver_build_mode = normalize_driver_build_mode(driver_build_mode) + compilation_mode = normalize_compilation_mode(compilation_mode) shutil.rmtree(os.path.join(source_dir, "src", "scenes"), ignore_errors=True) os.makedirs(os.path.join(source_dir, "src", "scenes"), exist_ok=True) @@ -372,13 +406,12 @@ async def make_local_modifications( for scene in frame.scenes: execution = scene.get("settings", {}).get("execution", "compiled") - safe_id = re.sub(r'\W+', '', scene.get('id', 'default')) if execution == "interpreted": # We're writing them to scenes.json post build continue try: scene_source = write_scene_nim(frame, scene) - with open(os.path.join(source_dir, "src", "scenes", f"scene_{safe_id}.nim"), "w") as f: + with open(os.path.join(source_dir, "src", "scenes", scene_module_filename(scene)), "w") as f: f.write(scene_source) except Exception as e: await self.log("stderr", @@ -386,13 +419,21 @@ async def make_local_modifications( f"({scene.get('id','default')}): {e}") raise + shared_scene_dir = os.path.join(source_dir, "src", "scenes", "shared") + shutil.rmtree(shared_scene_dir, ignore_errors=True) + if compilation_mode_uses_shared_libraries(compilation_mode): + os.makedirs(shared_scene_dir, exist_ok=True) + for scene in compiled_frame_scenes(frame): + with open(os.path.join(shared_scene_dir, scene_module_filename(scene)), "w") as sf: + sf.write(write_scene_library_nim(scene)) + with open(os.path.join(source_dir, "src", "scenes", "scenes.nim"), "w") as f: - source = write_scenes_nim(frame) + source = write_scenes_nim(frame, compilation_mode=compilation_mode) f.write(source) drivers = drivers_override or drivers_for_frame(frame) with open(os.path.join(source_dir, "src", "drivers", "drivers.nim"), "w") as f: - source = drivers_nim_source or write_drivers_nim(drivers, driver_build_mode=driver_build_mode) + source = drivers_nim_source or write_drivers_nim(drivers, compilation_mode=compilation_mode) f.write(source) if drivers.get("waveshare"): @@ -402,7 +443,7 @@ async def make_local_modifications( shared_driver_dir = os.path.join(source_dir, "src", "drivers", "shared") shutil.rmtree(shared_driver_dir, ignore_errors=True) - if driver_build_mode_uses_shared_libraries(driver_build_mode): + if compilation_mode_uses_shared_libraries(compilation_mode): os.makedirs(shared_driver_dir, exist_ok=True) for driver in compiled_drivers(drivers): with open(os.path.join(shared_driver_dir, f"{driver.name}.nim"), "w") as sf: @@ -502,8 +543,11 @@ def _write_c_makefile( linker_flags: Iterable[str], compiler_flags: Iterable[str], driver_dirs: list[str] | None = None, + scene_dirs: list[str] | None = None, ) -> None: driver_dirs = driver_dirs or [] + scene_dirs = scene_dirs or [] + library_dirs = driver_dirs + scene_dirs with open(template_path, "r") as mf_in, open(makefile_path, "w") as mk: for ln in mf_in.readlines(): if ln.startswith("EXECUTABLE = "): @@ -520,15 +564,19 @@ def _write_c_makefile( + " ".join([f for f in compiler_flags if f != "-c"]) + " $(EXTRA_CFLAGS)\n" ) - if driver_dirs and ln.startswith("all:"): - ln = "all: $(EXECUTABLE) driver-libraries\n" + if library_dirs and ln.startswith("all:"): + ln = "all: $(EXECUTABLE) shared-libraries\n" mk.write(ln) - if driver_dirs: - mk.write("\nDRIVER_DIRS = " + " ".join(driver_dirs) + "\n\n") - mk.write(".PHONY: driver-libraries $(DRIVER_DIRS)\n") + if library_dirs: + mk.write("\nLIBRARY_DIRS = " + " ".join(library_dirs) + "\n") + mk.write("DRIVER_DIRS = " + " ".join(driver_dirs) + "\n") + mk.write("SCENE_DIRS = " + " ".join(scene_dirs) + "\n\n") + mk.write(".PHONY: shared-libraries driver-libraries scene-libraries $(LIBRARY_DIRS)\n") + mk.write("shared-libraries: $(LIBRARY_DIRS)\n\n") mk.write("driver-libraries: $(DRIVER_DIRS)\n\n") - mk.write("$(DRIVER_DIRS):\n") + mk.write("scene-libraries: $(SCENE_DIRS)\n\n") + mk.write("$(LIBRARY_DIRS):\n") mk.write("\t+$(MAKE) -C $@\n") @staticmethod @@ -538,6 +586,7 @@ def _write_driver_makefile( output_name: str, linker_flags: Iterable[str], compiler_flags: Iterable[str], + library_kind: str = "driver", ) -> None: linker_flags_text = " ".join( FrameDeployer._dedupe_preserve_order(list(linker_flags) + list(DRIVER_LIBRARY_LDFLAGS)) @@ -549,7 +598,7 @@ def _write_driver_makefile( ) with open(makefile_path, "w") as mk: mk.write( - f"""# This Makefile is used for compiling driver C sources generated by Nim + f"""# This Makefile is used for compiling {library_kind} C sources generated by Nim CC ?= gcc STRIP ?= strip EXTRA_CFLAGS ?= @@ -575,7 +624,7 @@ def _write_driver_makefile( pre-build: \t@mkdir -p ../../../cache -\t@echo "🟣 Compiling driver $(LIBRARY)" +\t@echo "🟣 Compiling {library_kind} $(LIBRARY)" $(OBJECTS): pre-build @@ -665,7 +714,7 @@ async def create_local_build_archive( build_dir: str, source_dir: str, arch: str, - driver_build_mode: str = DEFAULT_DRIVER_BUILD_MODE, + compilation_mode: str = DEFAULT_COMPILATION_MODE, drivers_override: dict[str, Driver] | None = None, ) -> str: db = self.db @@ -745,16 +794,17 @@ async def create_local_build_archive( shutil.copy(nimbase_path, os.path.join(build_dir, "nimbase.h")) - driver_build_mode = normalize_driver_build_mode(driver_build_mode) + compilation_mode = normalize_compilation_mode(compilation_mode) driver_make_dirs: list[str] = [] - if driver_build_mode == DRIVER_BUILD_MODE_STATIC: + scene_make_dirs: list[str] = [] + if compilation_mode == COMPILATION_MODE_STATIC: self._copy_waveshare_build_files(source_dir, build_dir, drivers) script_path = self._find_compile_script(build_dir, "compile_frameos.sh") linker_flags, compiler_flags = self._extract_compile_flags(script_path, "frameos") main_driver_linker_flags = ( self._driver_linker_flags(drivers) - if driver_build_mode == DRIVER_BUILD_MODE_STATIC + if compilation_mode == COMPILATION_MODE_STATIC else [] ) linker_flags = self._dedupe_preserve_order( @@ -763,7 +813,7 @@ async def create_local_build_archive( + main_driver_linker_flags ) - if driver_build_mode_uses_shared_libraries(driver_build_mode): + if compilation_mode_uses_shared_libraries(compilation_mode): for driver in compiled_drivers(drivers): driver_dir = os.path.join(build_dir, "drivers", driver.name) os.makedirs(driver_dir, exist_ok=True) @@ -802,6 +852,42 @@ async def create_local_build_archive( ) driver_make_dirs.append(os.path.join("drivers", driver.name)) + for scene in compiled_frame_scenes(frame): + scene_dir_name = scene_module_suffix(scene) + scene_dir = os.path.join(build_dir, "scenes", scene_dir_name) + os.makedirs(scene_dir, exist_ok=True) + output_name = scene_library_filename(scene) + await self.log("stdout", f"🔥 Generating C sources for scene {scene.get('id', 'default')}.") + scene_cmd = ( + f"cd {source_dir} && {nim_path} compile --app:lib --os:linux --cpu:{cpu} " + f"--define:frameosSharedLibrary {' '.join(SHARED_LIBRARY_NIM_FLAGS)} " + f"--compileOnly --genScript --nimcache:{scene_dir} --out:{output_name} " + f"{debug_options} src/scenes/shared/{scene_module_filename(scene)} 2>&1" + ) + scene_status, scene_out, scene_err = await exec_local_command(db, redis, frame, scene_cmd) + if scene_status != 0: + raise Exception( + f"Failed to generate scene library sources for {scene.get('id', 'default')}: " + f"{scene_err or scene_out or 'see logs'}" + ) + shutil.copy(nimbase_path, os.path.join(scene_dir, "nimbase.h")) + + scene_script_path = self._find_compile_script(scene_dir) + scene_linker_flags, scene_compiler_flags = self._extract_compile_flags( + scene_script_path, output_name + ) + scene_linker_flags = self._dedupe_preserve_order( + scene_linker_flags + ["../../quickjs/libquickjs.a"] + ) + self._write_driver_makefile( + makefile_path=os.path.join(scene_dir, "Makefile"), + output_name=output_name, + linker_flags=scene_linker_flags, + compiler_flags=scene_compiler_flags, + library_kind="scene", + ) + scene_make_dirs.append(os.path.join("scenes", scene_dir_name)) + self._write_c_makefile( makefile_path=os.path.join(build_dir, "Makefile"), template_path=os.path.join(source_dir, "tools", "nimc.Makefile"), @@ -809,6 +895,7 @@ async def create_local_build_archive( linker_flags=linker_flags, compiler_flags=compiler_flags, driver_dirs=driver_make_dirs, + scene_dirs=scene_make_dirs, ) archive_path = os.path.join(temp_dir, f"build_{build_id}.tar.gz") diff --git a/backend/app/tasks/binary_builder.py b/backend/app/tasks/binary_builder.py index b7ebb66a3..6aa4d32eb 100644 --- a/backend/app/tasks/binary_builder.py +++ b/backend/app/tasks/binary_builder.py @@ -14,9 +14,9 @@ from app.tasks._frame_deployer import FrameDeployer from app.drivers.devices import drivers_for_frame from app.codegen.drivers_nim import ( - DRIVER_BUILD_MODE_PRECOMPILED, - frame_driver_build_mode, - normalize_driver_build_mode, + COMPILATION_MODE_PRECOMPILED, + frame_compilation_mode, + normalize_compilation_mode, ) from app.tasks.precompiled_frameos import ( download_precompiled_frameos_release, @@ -53,7 +53,7 @@ def should_suggest_clearing_build_cache(error_message: str) -> bool: class FrameBinaryPlan: build_id: str target: TargetMetadata - driver_build_mode: str + compilation_mode: str allow_cross_compile: bool force_cross_compile: bool cross_compile_supported: bool @@ -73,7 +73,7 @@ def to_dict(self) -> dict[str, object]: "distro": self.target.distro, "version": self.target.version, }, - "driver_build_mode": self.driver_build_mode, + "compilation_mode": self.compilation_mode, "allow_cross_compile": self.allow_cross_compile, "force_cross_compile": self.force_cross_compile, "cross_compile_supported": self.cross_compile_supported, @@ -91,13 +91,15 @@ def to_dict(self) -> dict[str, object]: class FrameBinaryBuildResult: build_id: str target: TargetMetadata - driver_build_mode: str + compilation_mode: str source_dir: str build_dir: str archive_path: str binary_path: str | None driver_library_paths: list[str] driver_library_names: list[str] + scene_library_paths: list[str] + scene_library_names: list[str] cross_compiled: bool prebuilt_entry: PrebuiltEntry | None prebuilt_target: str | None @@ -171,11 +173,11 @@ async def plan_build( allow_cross_compile: bool = True, force_cross_compile: bool = False, target_override: TargetMetadata | None = None, - driver_build_mode: str | None = None, + compilation_mode: str | None = None, ) -> FrameBinaryPlan: target = target_override or await self._detect_target() - resolved_driver_build_mode = normalize_driver_build_mode( - driver_build_mode or frame_driver_build_mode(self.frame) + resolved_compilation_mode = normalize_compilation_mode( + compilation_mode or frame_compilation_mode(self.frame) ) prebuilt_entry, prebuilt_target = await resolve_prebuilt_entry( distro=target.distro, @@ -186,7 +188,7 @@ async def plan_build( will_attempt_precompiled = False precompiled_url = None precompiled_skip_reason = None - if resolved_driver_build_mode == DRIVER_BUILD_MODE_PRECOMPILED: + if resolved_compilation_mode == COMPILATION_MODE_PRECOMPILED: compiled_scene_count = frame_compiled_scene_count(self.frame) precompiled_url = precompiled_frameos_release_url(prebuilt_target or "") if compiled_scene_count > 0: @@ -211,7 +213,7 @@ async def plan_build( return FrameBinaryPlan( build_id=self.deployer.build_id, target=target, - driver_build_mode=resolved_driver_build_mode, + compilation_mode=resolved_compilation_mode, allow_cross_compile=allow_cross_compile, force_cross_compile=force_cross_compile, cross_compile_supported=cross_compile_supported, @@ -234,7 +236,7 @@ async def build(self, plan: FrameBinaryPlan) -> FrameBinaryBuildResult: self.temp_dir, source_root=self.source_root ) await self._log("stdout", f"{icon} Applying local modifications") - await self.deployer.make_local_modifications(source_dir, driver_build_mode=plan.driver_build_mode) + await self.deployer.make_local_modifications(source_dir, compilation_mode=plan.compilation_mode) if self.db: await copy_custom_fonts_to_local_source_folder(self.db, source_dir) @@ -257,13 +259,15 @@ async def build(self, plan: FrameBinaryPlan) -> FrameBinaryBuildResult: return FrameBinaryBuildResult( build_id=self.deployer.build_id, target=plan.target, - driver_build_mode=plan.driver_build_mode, + compilation_mode=plan.compilation_mode, source_dir=source_dir, build_dir=build_dir, archive_path=precompiled_result.archive_path, binary_path=precompiled_result.binary_path, driver_library_paths=precompiled_result.driver_library_paths, driver_library_names=precompiled_result.driver_library_names, + scene_library_paths=precompiled_result.scene_library_paths, + scene_library_names=precompiled_result.scene_library_names, cross_compiled=True, prebuilt_entry=plan.prebuilt_entry, prebuilt_target=plan.prebuilt_target, @@ -273,7 +277,7 @@ async def build(self, plan: FrameBinaryPlan) -> FrameBinaryBuildResult: await self._log("stdout", f"{icon} Creating build archive") archive_path = await self.deployer.create_local_build_archive( - build_dir, source_dir, plan.target.arch, driver_build_mode=plan.driver_build_mode + build_dir, source_dir, plan.target.arch, compilation_mode=plan.compilation_mode ) cross_compiled = False @@ -346,7 +350,7 @@ async def build(self, plan: FrameBinaryPlan) -> FrameBinaryBuildResult: return FrameBinaryBuildResult( build_id=self.deployer.build_id, target=plan.target, - driver_build_mode=plan.driver_build_mode, + compilation_mode=plan.compilation_mode, source_dir=source_dir, build_dir=build_dir, archive_path=archive_path, @@ -354,11 +358,20 @@ async def build(self, plan: FrameBinaryPlan) -> FrameBinaryBuildResult: driver_library_paths=self.deployer.driver_library_paths( build_dir, drivers_for_frame(self.frame), - plan.driver_build_mode, + plan.compilation_mode, ), driver_library_names=self.deployer.driver_library_names( drivers_for_frame(self.frame), - plan.driver_build_mode, + plan.compilation_mode, + ), + scene_library_paths=self.deployer.scene_library_paths( + build_dir, + self.frame, + plan.compilation_mode, + ), + scene_library_names=self.deployer.scene_library_names( + self.frame, + plan.compilation_mode, ), cross_compiled=cross_compiled, prebuilt_entry=plan.prebuilt_entry, diff --git a/backend/app/tasks/frame_deploy_workflow.py b/backend/app/tasks/frame_deploy_workflow.py index 166a4ac42..c887d19bb 100644 --- a/backend/app/tasks/frame_deploy_workflow.py +++ b/backend/app/tasks/frame_deploy_workflow.py @@ -11,7 +11,7 @@ from sqlalchemy.orm import Session from app.drivers.devices import drivers_for_frame -from app.codegen.drivers_nim import DRIVER_BUILD_MODE_PRECOMPILED, normalize_driver_build_mode +from app.codegen.drivers_nim import COMPILATION_MODE_PRECOMPILED, normalize_compilation_mode from app.models.assets import sync_assets from app.models.frame import Frame, normalize_https_proxy, update_frame from app.models.log import new_log as log @@ -329,14 +329,14 @@ async def _plan_full(self, *, frame_dict: dict[str, Any], previous_frameos_versi cross_compilation_setting = (rpios_settings.get("crossCompilation") or "auto").lower() if cross_compilation_setting not in {"auto", "always", "never"}: cross_compilation_setting = "auto" - driver_build_mode = normalize_driver_build_mode(rpios_settings.get("driverBuildMode")) + compilation_mode = normalize_compilation_mode(rpios_settings.get("compilationMode")) allow_cross_compile = cross_compilation_setting != "never" force_cross_compile = cross_compilation_setting == "always" binary_plan = await self.binary_builder.plan_build( allow_cross_compile=allow_cross_compile, force_cross_compile=force_cross_compile, - driver_build_mode=driver_build_mode, + compilation_mode=compilation_mode, ) settings = get_settings_dict(self.db) @@ -449,9 +449,9 @@ async def _plan_full(self, *, frame_dict: dict[str, Any], previous_frameos_versi notes = [ f"Detected distro: {distro} ({distro_version}), architecture: {arch}, total memory: {total_memory} MiB", f"Cross compilation setting: {cross_compilation_setting}", - f"Driver build mode: {driver_build_mode}", + f"Compilation mode: {compilation_mode}", ] - if driver_build_mode == DRIVER_BUILD_MODE_PRECOMPILED: + if compilation_mode == COMPILATION_MODE_PRECOMPILED: if binary_plan.will_attempt_precompiled: notes.append("Precompiled FrameOS release will be used because all scenes are interpreted.") else: @@ -735,15 +735,32 @@ async def _publish_cross_compiled_driver_libraries( build_result: FrameBinaryBuildResult, build_id: str, ) -> None: - if not build_result.driver_library_paths: + await self._publish_cross_compiled_libraries( + local_paths=build_result.driver_library_paths, + label="driver", + remote_dir=self._release_driver_dir(build_id), + ) + await self._publish_cross_compiled_libraries( + local_paths=build_result.scene_library_paths, + label="scene", + remote_dir=self._release_scene_dir(build_id), + ) + + async def _publish_cross_compiled_libraries( + self, + *, + local_paths: list[str], + label: str, + remote_dir: str, + ) -> None: + if not local_paths: return - await self.deployer.log("stdout", f"{icon} Uploading shared driver libraries") - release_driver_dir = self._release_driver_dir(build_id) - await self.deployer.exec_command(f"mkdir -p {shlex.quote(release_driver_dir)}") - for local_path in build_result.driver_library_paths: + await self.deployer.log("stdout", f"{icon} Uploading shared {label} libraries") + await self.deployer.exec_command(f"mkdir -p {shlex.quote(remote_dir)}") + for local_path in local_paths: if not os.path.isfile(local_path): - raise RuntimeError(f"Shared driver library missing after cross compilation: {local_path}") - remote_path = f"{release_driver_dir}/{os.path.basename(local_path)}" + raise RuntimeError(f"Shared {label} library missing after cross compilation: {local_path}") + remote_path = f"{remote_dir}/{os.path.basename(local_path)}" await upload_binary(self.deployer, local_path, remote_path) async def _publish_remote_built_binary( @@ -787,13 +804,17 @@ async def _publish_remote_built_binary( await self.deployer.exec_command( f"cp {remote_build_dir}/frameos {release_frameos_path}" ) - if build_result.driver_library_paths: - release_driver_dir = self._release_driver_dir(build_id) - await self.deployer.exec_command(f"mkdir -p {shlex.quote(release_driver_dir)}") - for local_path in build_result.driver_library_paths: + for local_paths, release_dir in ( + (build_result.driver_library_paths, self._release_driver_dir(build_id)), + (build_result.scene_library_paths, self._release_scene_dir(build_id)), + ): + if not local_paths: + continue + await self.deployer.exec_command(f"mkdir -p {shlex.quote(release_dir)}") + for local_path in local_paths: relative_path = os.path.relpath(local_path, build_result.build_dir) remote_source = f"{remote_build_dir}/{relative_path}" - remote_dest = f"{release_driver_dir}/{os.path.basename(local_path)}" + remote_dest = f"{release_dir}/{os.path.basename(local_path)}" await self.deployer.exec_command( f"cp {shlex.quote(remote_source)} {shlex.quote(remote_dest)}" ) @@ -927,6 +948,10 @@ def _release_frameos_path(cls, build_id: str) -> str: def _release_driver_dir(cls, build_id: str) -> str: return f"{cls._release_dir(build_id)}/drivers" + @classmethod + def _release_scene_dir(cls, build_id: str) -> str: + return f"{cls._release_dir(build_id)}/scenes" + @staticmethod def _remote_build_dir(build_id: str) -> str: return f"/srv/frameos/build/build_{build_id}" diff --git a/backend/app/tasks/precompiled_frameos.py b/backend/app/tasks/precompiled_frameos.py index 6647e0511..fd7fce966 100644 --- a/backend/app/tasks/precompiled_frameos.py +++ b/backend/app/tasks/precompiled_frameos.py @@ -13,7 +13,7 @@ import httpx -from app.codegen.drivers_nim import DRIVER_BUILD_MODE_SHARED +from app.codegen.drivers_nim import COMPILATION_MODE_SHARED from app.drivers.devices import drivers_for_frame from app.models.frame import Frame from app.tasks._frame_deployer import FrameDeployer @@ -34,6 +34,8 @@ class PrecompiledFrameOSResult: binary_path: str driver_library_paths: list[str] driver_library_names: list[str] + scene_library_paths: list[str] + scene_library_names: list[str] vendor_folders: list[str] archive_path: str cache_hit: bool = False @@ -99,7 +101,7 @@ async def download_precompiled_frameos_release( required_driver_names = FrameDeployer.driver_library_names( drivers_for_frame(frame), - DRIVER_BUILD_MODE_SHARED, + COMPILATION_MODE_SHARED, ) copied_driver_paths = _copy_required_drivers( artifact_root=artifact_root, @@ -126,6 +128,8 @@ async def download_precompiled_frameos_release( binary_path=str(binary_dest), driver_library_paths=[str(path) for path in copied_driver_paths], driver_library_names=required_driver_names, + scene_library_paths=[], + scene_library_names=[], vendor_folders=vendor_folders, archive_path=result_archive, cache_hit=cache_hit, diff --git a/backend/app/tasks/tests/test_binary_builder.py b/backend/app/tasks/tests/test_binary_builder.py index a4fc0f8b8..364a7a23d 100644 --- a/backend/app/tasks/tests/test_binary_builder.py +++ b/backend/app/tasks/tests/test_binary_builder.py @@ -4,7 +4,7 @@ import pytest -from app.codegen.drivers_nim import DRIVER_BUILD_MODE_PRECOMPILED, DRIVER_BUILD_MODE_SHARED, DRIVER_BUILD_MODE_STATIC +from app.codegen.drivers_nim import COMPILATION_MODE_PRECOMPILED, COMPILATION_MODE_SHARED, COMPILATION_MODE_STATIC from app.tasks.binary_builder import FrameBinaryBuilder, FrameBinaryPlan from app.tasks.precompiled_frameos import PrecompiledFrameOSResult, release_version from app.utils.cross_compile import TargetMetadata @@ -15,7 +15,7 @@ def __init__(self) -> None: self.build_id = "build12345678" async def make_local_modifications( - self, _source_dir: str, driver_build_mode: str = DRIVER_BUILD_MODE_SHARED + self, _source_dir: str, compilation_mode: str = COMPILATION_MODE_SHARED ) -> None: return None @@ -27,19 +27,25 @@ async def create_local_build_archive( _build_dir: str, _source_dir: str, _arch: str, - driver_build_mode: str = DRIVER_BUILD_MODE_SHARED, + compilation_mode: str = COMPILATION_MODE_SHARED, ) -> str: return "/tmp/build.tar.gz" - def driver_library_paths(self, _build_dir, _drivers, _driver_build_mode): + def driver_library_paths(self, _build_dir, _drivers, _compilation_mode): return [] - def driver_library_names(self, _drivers, _driver_build_mode): + def driver_library_names(self, _drivers, _compilation_mode): + return [] + + def scene_library_paths(self, _build_dir, _frame, _compilation_mode): + return [] + + def scene_library_names(self, _frame, _compilation_mode): return [] @pytest.mark.asyncio -async def test_plan_build_defaults_to_static_driver_mode(monkeypatch: pytest.MonkeyPatch): +async def test_plan_build_defaults_to_static_compilation_mode(monkeypatch: pytest.MonkeyPatch): async def fake_resolve_prebuilt_entry(**_kwargs): return None, None @@ -59,11 +65,11 @@ async def fake_resolve_prebuilt_entry(**_kwargs): ) explicit_shared_plan = await builder.plan_build( target_override=TargetMetadata(arch="aarch64", distro="raspios", version="trixie"), - driver_build_mode=DRIVER_BUILD_MODE_SHARED, + compilation_mode=COMPILATION_MODE_SHARED, ) - assert plan.driver_build_mode == DRIVER_BUILD_MODE_STATIC - assert explicit_shared_plan.driver_build_mode == DRIVER_BUILD_MODE_SHARED + assert plan.compilation_mode == COMPILATION_MODE_STATIC + assert explicit_shared_plan.compilation_mode == COMPILATION_MODE_SHARED @pytest.mark.asyncio @@ -80,7 +86,7 @@ async def fake_resolve_prebuilt_entry(**_kwargs): frame=SimpleNamespace( device="framebuffer", gpio_buttons=[], - rpios={"driverBuildMode": "precompiled"}, + rpios={"compilationMode": "precompiled"}, scenes=[{"settings": {"execution": "interpreted"}}], ), deployer=FakeDeployer(), @@ -91,7 +97,7 @@ async def fake_resolve_prebuilt_entry(**_kwargs): target_override=TargetMetadata(arch="aarch64", distro="debian", version="trixie") ) - assert plan.driver_build_mode == DRIVER_BUILD_MODE_PRECOMPILED + assert plan.compilation_mode == COMPILATION_MODE_PRECOMPILED assert plan.will_attempt_precompiled is True assert plan.will_attempt_cross_compile is False assert plan.precompiled_release_url is not None @@ -115,7 +121,7 @@ async def fake_resolve_prebuilt_entry(**_kwargs): frame=SimpleNamespace( device="framebuffer", gpio_buttons=[], - rpios={"driverBuildMode": "precompiled"}, + rpios={"compilationMode": "precompiled"}, scenes=[{"settings": {"execution": "compiled"}}], ), deployer=FakeDeployer(), @@ -126,7 +132,7 @@ async def fake_resolve_prebuilt_entry(**_kwargs): target_override=TargetMetadata(arch="aarch64", distro="debian", version="trixie") ) - assert plan.driver_build_mode == DRIVER_BUILD_MODE_PRECOMPILED + assert plan.compilation_mode == COMPILATION_MODE_PRECOMPILED assert plan.will_attempt_precompiled is False assert plan.will_attempt_cross_compile is True assert plan.precompiled_skip_reason == "1 compiled scene is configured" @@ -153,7 +159,7 @@ async def fake_build_binary_with_cross_toolchain(**kwargs): plan = FrameBinaryPlan( build_id="build12345678", target=TargetMetadata(arch="aarch64", distro="raspios", version="trixie"), - driver_build_mode="static", + compilation_mode="static", allow_cross_compile=True, force_cross_compile=False, cross_compile_supported=True, @@ -193,6 +199,8 @@ async def fake_download_precompiled_frameos_release(**kwargs): binary_path=binary_path, driver_library_paths=[driver_path], driver_library_names=["frameBuffer.so"], + scene_library_paths=[], + scene_library_names=[], vendor_folders=[], archive_path=archive_path, ) @@ -213,7 +221,7 @@ async def fake_download_precompiled_frameos_release(**kwargs): plan = FrameBinaryPlan( build_id="build12345678", target=TargetMetadata(arch="aarch64", distro="debian", version="trixie"), - driver_build_mode="precompiled", + compilation_mode="precompiled", allow_cross_compile=True, force_cross_compile=False, cross_compile_supported=True, diff --git a/backend/app/tasks/tests/test_driver_build_mode.py b/backend/app/tasks/tests/test_compilation_mode.py similarity index 65% rename from backend/app/tasks/tests/test_driver_build_mode.py rename to backend/app/tasks/tests/test_compilation_mode.py index 0e66e2ad9..4eef7edfa 100644 --- a/backend/app/tasks/tests/test_driver_build_mode.py +++ b/backend/app/tasks/tests/test_compilation_mode.py @@ -3,13 +3,13 @@ from types import SimpleNamespace from app.codegen.drivers_nim import ( - DRIVER_BUILD_MODE_PRECOMPILED, - DRIVER_BUILD_MODE_SHARED, - DRIVER_BUILD_MODE_STATIC, - driver_build_mode_uses_shared_libraries, + COMPILATION_MODE_PRECOMPILED, + COMPILATION_MODE_SHARED, + COMPILATION_MODE_STATIC, + compilation_mode_uses_shared_libraries, driver_library_filename, - frame_driver_build_mode, - normalize_driver_build_mode, + frame_compilation_mode, + normalize_compilation_mode, write_driver_library_nim, write_shared_drivers_nim, write_static_drivers_nim, @@ -17,30 +17,30 @@ from app.drivers.drivers import Driver -def test_driver_build_mode_defaults_to_static(): - assert normalize_driver_build_mode(None) == DRIVER_BUILD_MODE_STATIC - assert normalize_driver_build_mode("") == DRIVER_BUILD_MODE_STATIC - assert normalize_driver_build_mode("unexpected") == DRIVER_BUILD_MODE_STATIC - assert frame_driver_build_mode(SimpleNamespace(rpios=None)) == DRIVER_BUILD_MODE_STATIC - assert frame_driver_build_mode(SimpleNamespace(rpios={})) == DRIVER_BUILD_MODE_STATIC +def test_compilation_mode_defaults_to_static(): + assert normalize_compilation_mode(None) == COMPILATION_MODE_STATIC + assert normalize_compilation_mode("") == COMPILATION_MODE_STATIC + assert normalize_compilation_mode("unexpected") == COMPILATION_MODE_STATIC + assert frame_compilation_mode(SimpleNamespace(rpios=None)) == COMPILATION_MODE_STATIC + assert frame_compilation_mode(SimpleNamespace(rpios={})) == COMPILATION_MODE_STATIC -def test_driver_build_mode_shared_requires_explicit_setting(): - assert normalize_driver_build_mode("shared") == DRIVER_BUILD_MODE_SHARED - assert frame_driver_build_mode(SimpleNamespace(rpios={"driverBuildMode": "shared"})) == DRIVER_BUILD_MODE_SHARED +def test_compilation_mode_shared_requires_explicit_setting(): + assert normalize_compilation_mode("shared") == COMPILATION_MODE_SHARED + assert frame_compilation_mode(SimpleNamespace(rpios={"compilationMode": "shared"})) == COMPILATION_MODE_SHARED -def test_driver_build_mode_static_is_valid(): - assert normalize_driver_build_mode("static") == DRIVER_BUILD_MODE_STATIC - assert frame_driver_build_mode(SimpleNamespace(rpios={"driverBuildMode": "static"})) == DRIVER_BUILD_MODE_STATIC +def test_compilation_mode_static_is_valid(): + assert normalize_compilation_mode("static") == COMPILATION_MODE_STATIC + assert frame_compilation_mode(SimpleNamespace(rpios={"compilationMode": "static"})) == COMPILATION_MODE_STATIC -def test_driver_build_mode_precompiled_uses_shared_libraries(): - assert normalize_driver_build_mode("precompiled") == DRIVER_BUILD_MODE_PRECOMPILED - assert frame_driver_build_mode(SimpleNamespace(rpios={"driverBuildMode": "precompiled"})) == DRIVER_BUILD_MODE_PRECOMPILED - assert driver_build_mode_uses_shared_libraries("precompiled") is True - assert driver_build_mode_uses_shared_libraries("shared") is True - assert driver_build_mode_uses_shared_libraries("static") is False +def test_compilation_mode_precompiled_uses_shared_libraries(): + assert normalize_compilation_mode("precompiled") == COMPILATION_MODE_PRECOMPILED + assert frame_compilation_mode(SimpleNamespace(rpios={"compilationMode": "precompiled"})) == COMPILATION_MODE_PRECOMPILED + assert compilation_mode_uses_shared_libraries("precompiled") is True + assert compilation_mode_uses_shared_libraries("shared") is True + assert compilation_mode_uses_shared_libraries("static") is False def test_waveshare_driver_library_filename_includes_variant(): diff --git a/backend/app/tasks/tests/test_cross_bin.py b/backend/app/tasks/tests/test_cross_bin.py index 726612ca8..3cfa1af79 100644 --- a/backend/app/tasks/tests/test_cross_bin.py +++ b/backend/app/tasks/tests/test_cross_bin.py @@ -48,7 +48,7 @@ def __init__(self, **kwargs): async def plan_build(self, **kwargs): FakeBinaryBuilder.last_plan_kwargs = kwargs - return SimpleNamespace(marker="plan", driver_build_mode=kwargs.get("driver_build_mode") or "static") + return SimpleNamespace(marker="plan", compilation_mode=kwargs.get("compilation_mode") or "static") async def build(self, plan): FakeBinaryBuilder.last_build_plan = plan @@ -56,6 +56,8 @@ async def build(self, plan): binary_path=str(binary_path), driver_library_paths=[], driver_library_names=[], + scene_library_paths=[], + scene_library_names=[], ) monkeypatch.setattr("backend.app.tasks._frame_deployer.FrameDeployer", FakeFrameDeployer) @@ -78,7 +80,7 @@ async def build(self, plan): assert destination.exists() assert destination.read_bytes() == b"frameos" metadata = json.loads((artifacts_dir / "debian-trixie-amd64" / "metadata.json").read_text(encoding="utf-8")) - assert metadata["driver_build_mode"] == "static" + assert metadata["compilation_mode"] == "static" @pytest.mark.asyncio @@ -129,11 +131,11 @@ async def create_local_build_archive(self, build_dir, source_dir, arch, **kwargs return str(Path(self.temp_dir) / "build_release12345.tar.gz") @staticmethod - def driver_library_paths(build_dir, _drivers, _driver_build_mode): + def driver_library_paths(build_dir, _drivers, _compilation_mode): return [str(Path(build_dir) / "drivers" / "httpUpload" / "httpUpload.so")] @staticmethod - def driver_library_names(_drivers, _driver_build_mode): + def driver_library_names(_drivers, _compilation_mode): return ["httpUpload.so"] class FakeCrossCompiler: diff --git a/backend/app/tasks/tests/test_frame_deploy_workflow.py b/backend/app/tasks/tests/test_frame_deploy_workflow.py index a0924c7b7..e654c3060 100644 --- a/backend/app/tasks/tests/test_frame_deploy_workflow.py +++ b/backend/app/tasks/tests/test_frame_deploy_workflow.py @@ -97,7 +97,7 @@ async def plan_build(self, **_kwargs) -> FrameBinaryPlan: return FrameBinaryPlan( build_id="build12345678", target=TargetMetadata(arch="arm64", distro="raspios", version="bookworm"), - driver_build_mode="static", + compilation_mode="static", allow_cross_compile=True, force_cross_compile=False, cross_compile_supported=True, @@ -113,7 +113,7 @@ async def plan_build(self, **_kwargs) -> FrameBinaryPlan: return FrameBinaryPlan( build_id="build12345678", target=TargetMetadata(arch="arm64", distro="raspios", version="bookworm"), - driver_build_mode="precompiled", + compilation_mode="precompiled", allow_cross_compile=True, force_cross_compile=False, cross_compile_supported=True, @@ -132,7 +132,7 @@ async def test_full_plan_defaults_to_single_executable(monkeypatch: pytest.Monke class CapturingBinaryBuilder(FakeBinaryBuilder): async def plan_build(self, **kwargs) -> FrameBinaryPlan: - captured_modes.append(kwargs["driver_build_mode"]) + captured_modes.append(kwargs["compilation_mode"]) return await super().plan_build(**kwargs) frame = SimpleNamespace( @@ -170,14 +170,14 @@ async def test_full_plan_uses_shared_driver_libraries_when_explicit(monkeypatch: class CapturingBinaryBuilder(FakeBinaryBuilder): async def plan_build(self, **kwargs) -> FrameBinaryPlan: - captured_modes.append(kwargs["driver_build_mode"]) + captured_modes.append(kwargs["compilation_mode"]) return await super().plan_build(**kwargs) frame = SimpleNamespace( id=3, name="SharedExplicit", ssh_keys=[], - rpios={"crossCompilation": "auto", "driverBuildMode": "shared"}, + rpios={"crossCompilation": "auto", "compilationMode": "shared"}, reboot=None, last_successful_deploy={"frameos_version": "9.9.9"}, last_successful_deploy_at="2026-01-01T00:00:00+00:00", @@ -337,7 +337,7 @@ async def test_full_plan_skips_remote_build_dependencies_for_precompiled(monkeyp id=17, name="PrecompiledFrame", ssh_keys=[], - rpios={"crossCompilation": "auto", "driverBuildMode": "precompiled"}, + rpios={"crossCompilation": "auto", "compilationMode": "precompiled"}, reboot=None, last_successful_deploy={"frameos_version": "9.9.9"}, last_successful_deploy_at="2026-01-01T00:00:00+00:00", @@ -891,6 +891,7 @@ async def fake_build_full_release_binary(_full_plan): prebuilt_entry=None, build_dir="/tmp/build", driver_library_paths=[], + scene_library_paths=[], ) async def fake_prepare_remote_for_full_release(**_kwargs): @@ -985,6 +986,7 @@ async def fake_upload_file(_db, _redis, _frame, remote_path, _data): archive_path=str(archive_path), build_dir=str(tmp_path / "build_build12345678"), driver_library_paths=[], + scene_library_paths=[], target=TargetMetadata(arch="x86_64", distro="debian", version="bookworm"), ), "build12345678", diff --git a/backend/app/tasks/tests/test_frame_deployer.py b/backend/app/tasks/tests/test_frame_deployer.py index e646db945..1fd06c063 100644 --- a/backend/app/tasks/tests/test_frame_deployer.py +++ b/backend/app/tasks/tests/test_frame_deployer.py @@ -195,7 +195,7 @@ async def fake_log(*_args, **_kwargs): build_dir = temp_dir / f"build_{deployer.build_id}" build_dir.mkdir() - await deployer.create_local_build_archive(str(build_dir), str(source_dir), "arm64", driver_build_mode="shared") + await deployer.create_local_build_archive(str(build_dir), str(source_dir), "arm64", compilation_mode="shared") assert len(commands) == 2 assert "src/drivers/shared/httpUpload.nim" in commands[1] @@ -205,7 +205,9 @@ async def fake_log(*_args, **_kwargs): assert "--lineTrace:off" in commands[1] assert "--passL:-Wl,--gc-sections" in commands[1] makefile_text = (build_dir / "Makefile").read_text(encoding="utf-8") + assert "LIBRARY_DIRS = drivers/httpUpload" in makefile_text assert "DRIVER_DIRS = drivers/httpUpload" in makefile_text + assert "shared-libraries: $(LIBRARY_DIRS)" in makefile_text assert "driver-libraries: $(DRIVER_DIRS)" in makefile_text assert "+$(MAKE) -C $@" in makefile_text assert "for dir in $(DRIVER_DIRS)" not in makefile_text @@ -225,6 +227,82 @@ async def fake_log(*_args, **_kwargs): assert "-Wl,--gc-sections" in driver_makefile_text +@pytest.mark.asyncio +async def test_create_local_build_archive_generates_shared_scene_makefiles( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +): + source_dir = tmp_path / "frameos" + temp_dir = tmp_path / "temp" + source_dir.mkdir() + temp_dir.mkdir() + (source_dir / "tools").mkdir() + (source_dir / "tools" / "nimc.Makefile").write_text( + "LIBS =\nCFLAGS =\nall: $(EXECUTABLE)\n", + encoding="utf-8", + ) + + nimbase = tmp_path / "nimbase.h" + nimbase.write_text("/* nimbase */\n", encoding="utf-8") + commands: list[str] = [] + + async def fake_exec_local_command(_db, _redis, _frame, cmd, **_kwargs): + commands.append(cmd) + nimcache = cmd.split("--nimcache:", 1)[1].split(" ", 1)[0] + cache_dir = Path(nimcache) + cache_dir.mkdir(parents=True, exist_ok=True) + if "src/frameos.nim" in cmd: + (cache_dir / "compile_frameos.sh").write_text( + "cc -c frameos.c -o frameos.o -Wall\n" + "cc frameos.o -o frameos -pthread -lm -ldl\n", + encoding="utf-8", + ) + else: + (cache_dir / "compile_scene_myscene.sh").write_text( + "cc -c scene.c -o scene.o -fPIC -Wall\n" + "cc scene.o -shared -o scene_myscene.so -pthread -lm -ldl\n", + encoding="utf-8", + ) + return 0, "", "" + + async def fake_log(*_args, **_kwargs): + return None + + monkeypatch.setattr("app.tasks._frame_deployer.exec_local_command", fake_exec_local_command) + monkeypatch.setattr("app.tasks._frame_deployer.find_nimbase_file", lambda _nim_path: str(nimbase)) + monkeypatch.setattr("app.tasks._frame_deployer.drivers_for_frame", lambda _frame: {}) + + frame = SimpleNamespace( + id=1, + debug=False, + scenes=[{"id": "my-scene", "name": "My Scene", "settings": {"execution": "compiled"}}], + ) + deployer = FrameDeployer( + db=None, + redis=None, + frame=frame, + nim_path="/usr/bin/nim", + temp_dir=str(temp_dir), + ) + deployer.log = fake_log # type: ignore[method-assign] + build_dir = temp_dir / f"build_{deployer.build_id}" + build_dir.mkdir() + + await deployer.create_local_build_archive(str(build_dir), str(source_dir), "arm64", compilation_mode="shared") + + assert len(commands) == 2 + assert "src/scenes/shared/scene_myscene.nim" in commands[1] + assert "--define:frameosSharedLibrary" in commands[1] + scene_makefile = build_dir / "scenes" / "myscene" / "Makefile" + scene_makefile_text = scene_makefile.read_text(encoding="utf-8") + assert "LIBRARY = scene_myscene.so" in scene_makefile_text + assert "Compiling scene $(LIBRARY)" in scene_makefile_text + makefile_text = (build_dir / "Makefile").read_text(encoding="utf-8") + assert "LIBRARY_DIRS = scenes/myscene" in makefile_text + assert "SCENE_DIRS = scenes/myscene" in makefile_text + assert "scene-libraries: $(SCENE_DIRS)" in makefile_text + + def test_evdev_shared_driver_wrapper_avoids_image_runtime_imports(): source = write_driver_library_nim(DRIVERS["evdev"]) diff --git a/backend/app/tasks/tests/test_real_ssh_deploy_e2e.py b/backend/app/tasks/tests/test_real_ssh_deploy_e2e.py index 6f04f497b..c3d2221a6 100644 --- a/backend/app/tasks/tests/test_real_ssh_deploy_e2e.py +++ b/backend/app/tasks/tests/test_real_ssh_deploy_e2e.py @@ -467,7 +467,7 @@ async def test_real_ssh_full_fast_cross_and_precompiled_deploy( db, ssh_target, name="DeployE2EPrecompiled", - rpios={"crossCompilation": "auto", "driverBuildMode": "precompiled"}, + rpios={"crossCompilation": "auto", "compilationMode": "precompiled"}, ) precompiled_plan = await _run_full_deploy( db, diff --git a/backend/app/utils/cross_compile.py b/backend/app/utils/cross_compile.py index 5e57b89c4..7cdbe6b0b 100644 --- a/backend/app/utils/cross_compile.py +++ b/backend/app/utils/cross_compile.py @@ -353,7 +353,7 @@ async def _run_remote_docker_build( local_binary = os.path.join(build_dir, "frameos") await host.download_file(f"{remote_build_dir}/frameos", local_binary) status, stdout, _err = await host.run( - f"cd {shlex.quote(remote_build_dir)} && find drivers -type f -name '*.so' 2>/dev/null || true", + f"cd {shlex.quote(remote_build_dir)} && find drivers scenes -type f -name '*.so' 2>/dev/null || true", log_command=False, log_output=False, ) diff --git a/backend/bin/cross b/backend/bin/cross index c21c2c7e2..2df0852ff 100755 --- a/backend/bin/cross +++ b/backend/bin/cross @@ -311,7 +311,7 @@ async def build_target( slug: str, frameos_root: Path, artifacts_dir: Path, - driver_build_mode: str | None = None, + compilation_mode: str | None = None, ) -> Path: if slug not in TARGET_MAP: raise SystemExit(f"Unknown target '{slug}'. Run --list-targets to see available options.") @@ -370,7 +370,7 @@ async def build_target( allow_cross_compile=True, force_cross_compile=True, target_override=target_override, - driver_build_mode=driver_build_mode, + compilation_mode=compilation_mode, ) build_result = await builder.build(build_plan) binary_path = build_result.binary_path @@ -390,6 +390,14 @@ async def build_target( if not src.is_file(): raise RuntimeError(f"Shared driver library missing after build: {src}") shutil.copy2(src, driver_dir / src.name) + if build_result.scene_library_paths: + scene_dir = target_dir / "scenes" + scene_dir.mkdir(parents=True, exist_ok=True) + for scene_library_path in build_result.scene_library_paths: + src = Path(scene_library_path) + if not src.is_file(): + raise RuntimeError(f"Shared scene library missing after build: {src}") + shutil.copy2(src, scene_dir / src.name) metadata = { "slug": slug, "arch": target.arch, @@ -397,8 +405,9 @@ async def build_target( "version": target.version, "platform": target.platform, "image": target.image, - "driver_build_mode": build_plan.driver_build_mode, + "compilation_mode": build_plan.compilation_mode, "driver_libraries": build_result.driver_library_names, + "scene_libraries": build_result.scene_library_names, } with (target_dir / "metadata.json").open("w", encoding="utf-8") as fh: json.dump(metadata, fh, indent=2) @@ -426,7 +435,7 @@ async def build_release_target( from backend.app.tasks.utils import find_nim_v2 # type: ignore # noqa: E402 from backend.app.utils import local_exec # type: ignore # noqa: E402 from backend.app.utils.cross_compile import CrossCompiler, TargetMetadata # type: ignore # noqa: E402 - from app.codegen.drivers_nim import DRIVER_BUILD_MODE_SHARED, compiled_drivers # type: ignore # noqa: E402 + from app.codegen.drivers_nim import COMPILATION_MODE_SHARED, compiled_drivers # type: ignore # noqa: E402 from app.codegen.release_drivers_nim import ( # type: ignore # noqa: E402 release_driver_specs, write_release_driver_libraries, @@ -448,7 +457,7 @@ async def build_release_target( release_drivers = release_driver_specs() driver_library_names = FrameDeployer.driver_library_names( release_drivers, - DRIVER_BUILD_MODE_SHARED, + COMPILATION_MODE_SHARED, ) driver_names = [driver.name for driver in compiled_drivers(release_drivers)] @@ -467,7 +476,7 @@ async def build_release_target( await deployer.log("stdout", "Preparing release driver registry") await deployer.make_local_modifications( source_dir, - driver_build_mode=DRIVER_BUILD_MODE_SHARED, + compilation_mode=COMPILATION_MODE_SHARED, drivers_override=release_drivers, drivers_nim_source=write_release_shared_drivers_nim(release_drivers), ) @@ -517,7 +526,7 @@ async def build_release_target( str(build_dir), source_dir, target.arch, - driver_build_mode=DRIVER_BUILD_MODE_SHARED, + compilation_mode=COMPILATION_MODE_SHARED, drivers_override=release_drivers, ) write_release_build_marker( @@ -531,7 +540,7 @@ async def build_release_target( driver_library_paths = deployer.driver_library_paths( str(build_dir), release_drivers, - DRIVER_BUILD_MODE_SHARED, + COMPILATION_MODE_SHARED, ) if not rebuild and release_build_outputs_complete( @@ -604,7 +613,7 @@ async def build_release_target( "platform": target.platform, "image": target.image, "release_artifact": True, - "driver_build_mode": DRIVER_BUILD_MODE_SHARED, + "compilation_mode": COMPILATION_MODE_SHARED, "driver_registry": "runtime-filtered", "driver_libraries": driver_library_names, "vendor_folders": vendor_folders, @@ -637,10 +646,11 @@ def parse_args(argv: Iterable[str]) -> argparse.Namespace: build.add_argument("--frameos-root", default=str(Path.cwd()), help="Path to the frameos source tree") build.add_argument("--artifacts-dir", default="build/cross", help="Directory for compiled binaries") build.add_argument( - "--driver-build-mode", + "--compilation-mode", + dest="compilation_mode", choices=("static", "shared", "precompiled"), default=None, - help="Override driver build mode (static, shared, or precompiled release when eligible)", + help="Override compilation mode (static, shared, or precompiled release when eligible)", ) release = subparsers.add_parser( @@ -693,7 +703,7 @@ def main(argv: list[str]) -> int: artifacts_dir = frameos_root / artifacts_dir async def _build() -> Path: - return await build_target(args.target, frameos_root, artifacts_dir, args.driver_build_mode) + return await build_target(args.target, frameos_root, artifacts_dir, args.compilation_mode) asyncio.run(_build()) return 0 diff --git a/e2e/Makefile b/e2e/Makefile index 2ee2b6e18..dab887b3d 100644 --- a/e2e/Makefile +++ b/e2e/Makefile @@ -33,8 +33,11 @@ snapshots: rm -rf "$$tmpdir/src/scenes" && \ cp -r ./generated "$$tmpdir/src/scenes" && \ cp -r ./generated/scenes.json "$$tmpdir/" && \ - (cd "$$tmpdir" && make build DRIVER_BUILD_MODE=shared SKIP_DRIVER_LIBRARIES=1) && \ + (cd "$$tmpdir" && make build COMPILATION_MODE=shared SKIP_DRIVER_LIBRARIES=1) && \ cp "$$tmpdir/build/frameos" ./tmp/frameos-bin && \ + rm -rf ./tmp/scenes && \ + mkdir -p ./tmp/scenes && \ + if [ -d "$$tmpdir/build/scenes" ]; then cp -r "$$tmpdir/build/scenes"/. ./tmp/scenes; fi && \ cp "$$tmpdir/scenes.json" ./tmp/scenes.json ls -l ./tmp/frameos-bin diff --git a/e2e/generated/scenes.nim b/e2e/generated/scenes.nim index c6964f685..a2c598c0f 100644 --- a/e2e/generated/scenes.nim +++ b/e2e/generated/scenes.nim @@ -1,37 +1,57 @@ # This file is autogenerated +import std/[dynlib, json, options, os, tables] import frameos/types -import tables, options -import scenes/scene_black as scene_black -import scenes/scene_blue as scene_blue -import scenes/scene_dataCodeFloat as scene_dataCodeFloat -import scenes/scene_dataDownloadImage as scene_dataDownloadImage -import scenes/scene_dataDownloadUrl as scene_dataDownloadUrl -import scenes/scene_dataGradient as scene_dataGradient -import scenes/scene_dataLocalImage as scene_dataLocalImage -import scenes/scene_dataNewImage as scene_dataNewImage -import scenes/scene_dataNewImageNext as scene_dataNewImageNext -import scenes/scene_dataQR as scene_dataQR -import scenes/scene_dataResize as scene_dataResize -import scenes/scene_logicIfElse as scene_logicIfElse -import scenes/scene_logicSetAsState as scene_logicSetAsState -import scenes/scene_renderColorFlow as scene_renderColorFlow -import scenes/scene_renderColorImage as scene_renderColorImage -import scenes/scene_renderColorSplit as scene_renderColorSplit -import scenes/scene_renderGradientSplit as scene_renderGradientSplit -import scenes/scene_renderImage as scene_renderImage -import scenes/scene_renderImageBlend as scene_renderImageBlend -import scenes/scene_renderImageMask as scene_renderImageMask -import scenes/scene_renderOpacity as scene_renderOpacity -import scenes/scene_renderSplitData as scene_renderSplitData -import scenes/scene_renderSplitFlow as scene_renderSplitFlow -import scenes/scene_renderSplitLoop as scene_renderSplitLoop -import scenes/scene_renderTextOverflow as scene_renderTextOverflow -import scenes/scene_renderTextPosition as scene_renderTextPosition -import scenes/scene_renderTextRich as scene_renderTextRich -import scenes/scene_renderTextRichOver as scene_renderTextRichOver -import scenes/scene_renderTextSplit as scene_renderTextSplit -import scenes/scene_sceneNodes as scene_sceneNodes +import frameos/channels as hostChannels +import frameos/driver_abi + +type + SceneSpec = object + id: SceneId + name: string + libraryName: string + + LoadedSceneLibrary = object + spec: SceneSpec + library: LibHandle + exportedScene: ExportedScene + + SceneInitProc = proc(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl.} + SceneExportProc = proc(): pointer {.cdecl.} + +let sceneSpecs: seq[SceneSpec] = @[ + SceneSpec(id: "black".SceneId, name: "Black", libraryName: "scene_black.so"), + SceneSpec(id: "blue".SceneId, name: "Blue", libraryName: "scene_blue.so"), + SceneSpec(id: "dataCodeFloat".SceneId, name: "Numeric Code Nodes", libraryName: "scene_dataCodeFloat.so"), + SceneSpec(id: "dataDownloadImage".SceneId, name: "Download Image", libraryName: "scene_dataDownloadImage.so"), + SceneSpec(id: "dataDownloadUrl".SceneId, name: "Download URL", libraryName: "scene_dataDownloadUrl.so"), + SceneSpec(id: "dataGradient".SceneId, name: "dataGradient", libraryName: "scene_dataGradient.so"), + SceneSpec(id: "dataLocalImage".SceneId, name: "Local Image", libraryName: "scene_dataLocalImage.so"), + SceneSpec(id: "dataNewImage".SceneId, name: "New Image", libraryName: "scene_dataNewImage.so"), + SceneSpec(id: "dataNewImageNext".SceneId, name: "Data Image Next", libraryName: "scene_dataNewImageNext.so"), + SceneSpec(id: "dataQR".SceneId, name: "QR", libraryName: "scene_dataQR.so"), + SceneSpec(id: "dataResize".SceneId, name: "Resize image", libraryName: "scene_dataResize.so"), + SceneSpec(id: "logicIfElse".SceneId, name: "If Else", libraryName: "scene_logicIfElse.so"), + SceneSpec(id: "logicSetAsState".SceneId, name: "Set as State", libraryName: "scene_logicSetAsState.so"), + SceneSpec(id: "renderColorFlow".SceneId, name: "Color", libraryName: "scene_renderColorFlow.so"), + SceneSpec(id: "renderColorImage".SceneId, name: "Color", libraryName: "scene_renderColorImage.so"), + SceneSpec(id: "renderColorSplit".SceneId, name: "Color", libraryName: "scene_renderColorSplit.so"), + SceneSpec(id: "renderGradientSplit".SceneId, name: "Gradient", libraryName: "scene_renderGradientSplit.so"), + SceneSpec(id: "renderImage".SceneId, name: "Render image", libraryName: "scene_renderImage.so"), + SceneSpec(id: "renderImageBlend".SceneId, name: "Blend Modes", libraryName: "scene_renderImageBlend.so"), + SceneSpec(id: "renderImageMask".SceneId, name: "Image Mask", libraryName: "scene_renderImageMask.so"), + SceneSpec(id: "renderOpacity".SceneId, name: "Opacity", libraryName: "scene_renderOpacity.so"), + SceneSpec(id: "renderSplitData".SceneId, name: "Split", libraryName: "scene_renderSplitData.so"), + SceneSpec(id: "renderSplitFlow".SceneId, name: "Split", libraryName: "scene_renderSplitFlow.so"), + SceneSpec(id: "renderSplitLoop".SceneId, name: "Split Loop", libraryName: "scene_renderSplitLoop.so"), + SceneSpec(id: "renderTextOverflow".SceneId, name: "Text Overflow", libraryName: "scene_renderTextOverflow.so"), + SceneSpec(id: "renderTextPosition".SceneId, name: "Text", libraryName: "scene_renderTextPosition.so"), + SceneSpec(id: "renderTextRich".SceneId, name: "Rich text", libraryName: "scene_renderTextRich.so"), + SceneSpec(id: "renderTextRichOver".SceneId, name: "Rich text overflow", libraryName: "scene_renderTextRichOver.so"), + SceneSpec(id: "renderTextSplit".SceneId, name: "Text Split", libraryName: "scene_renderTextSplit.so"), + SceneSpec(id: "sceneNodes".SceneId, name: "3", libraryName: "scene_sceneNodes.so") +] +var loadedSceneLibraries: seq[LoadedSceneLibrary] = @[] let defaultSceneId* = some("black".SceneId) @@ -68,35 +88,56 @@ const sceneOptions*: array[30, tuple[id: SceneId, name: string]] = [ ("sceneNodes".SceneId, "3"), ] +proc hostLog(event: JsonNode) {.cdecl, gcsafe.} = + hostChannels.log(event) + +proc hostSendEvent(scene: Option[SceneId], event: string, payload: JsonNode) {.cdecl, gcsafe.} = + hostChannels.sendEvent(scene, event, payload) + +proc sceneLibraryPath(spec: SceneSpec): string = + getAppDir() / "scenes" / spec.libraryName + +proc loadRequiredSymbol[T](library: LibHandle, sceneId: SceneId, symbol: string): T = + let address = symAddr(library, symbol) + if address.isNil: + hostChannels.log(%*{"event": "scene:shared:error", "sceneId": sceneId.string, + "error": "Missing symbol", "symbol": symbol}) + return nil + cast[T](address) + +proc loadSharedScene(spec: SceneSpec): Option[ExportedScene] = + let path = sceneLibraryPath(spec) + let library = loadLib(path) + if library.isNil: + hostChannels.log(%*{"event": "scene:shared:error", "sceneId": spec.id.string, + "error": "Unable to load scene library", "path": path}) + return none(ExportedScene) + + let initProc = loadRequiredSymbol[SceneInitProc](library, spec.id, "frameos_scene_init") + if initProc.isNil: + unloadLib(library) + return none(ExportedScene) + initProc(hostLog, hostSendEvent) + + let exportProc = loadRequiredSymbol[SceneExportProc](library, spec.id, "frameos_scene_export") + if exportProc.isNil: + unloadLib(library) + return none(ExportedScene) + + let exportedScene = cast[ExportedScene](exportProc()) + if exportedScene.isNil: + hostChannels.log(%*{"event": "scene:shared:error", "sceneId": spec.id.string, + "error": "Scene library returned nil export", "path": path}) + unloadLib(library) + return none(ExportedScene) + + loadedSceneLibraries.add(LoadedSceneLibrary(spec: spec, library: library, exportedScene: exportedScene)) + hostChannels.log(%*{"event": "scene:shared", "sceneId": spec.id.string, "path": path, "loaded": true}) + return some(exportedScene) + proc getExportedScenes*(): Table[SceneId, ExportedScene] = result = initTable[SceneId, ExportedScene]() - result["black".SceneId] = scene_black.exportedScene - result["blue".SceneId] = scene_blue.exportedScene - result["dataCodeFloat".SceneId] = scene_dataCodeFloat.exportedScene - result["dataDownloadImage".SceneId] = scene_dataDownloadImage.exportedScene - result["dataDownloadUrl".SceneId] = scene_dataDownloadUrl.exportedScene - result["dataGradient".SceneId] = scene_dataGradient.exportedScene - result["dataLocalImage".SceneId] = scene_dataLocalImage.exportedScene - result["dataNewImage".SceneId] = scene_dataNewImage.exportedScene - result["dataNewImageNext".SceneId] = scene_dataNewImageNext.exportedScene - result["dataQR".SceneId] = scene_dataQR.exportedScene - result["dataResize".SceneId] = scene_dataResize.exportedScene - result["logicIfElse".SceneId] = scene_logicIfElse.exportedScene - result["logicSetAsState".SceneId] = scene_logicSetAsState.exportedScene - result["renderColorFlow".SceneId] = scene_renderColorFlow.exportedScene - result["renderColorImage".SceneId] = scene_renderColorImage.exportedScene - result["renderColorSplit".SceneId] = scene_renderColorSplit.exportedScene - result["renderGradientSplit".SceneId] = scene_renderGradientSplit.exportedScene - result["renderImage".SceneId] = scene_renderImage.exportedScene - result["renderImageBlend".SceneId] = scene_renderImageBlend.exportedScene - result["renderImageMask".SceneId] = scene_renderImageMask.exportedScene - result["renderOpacity".SceneId] = scene_renderOpacity.exportedScene - result["renderSplitData".SceneId] = scene_renderSplitData.exportedScene - result["renderSplitFlow".SceneId] = scene_renderSplitFlow.exportedScene - result["renderSplitLoop".SceneId] = scene_renderSplitLoop.exportedScene - result["renderTextOverflow".SceneId] = scene_renderTextOverflow.exportedScene - result["renderTextPosition".SceneId] = scene_renderTextPosition.exportedScene - result["renderTextRich".SceneId] = scene_renderTextRich.exportedScene - result["renderTextRichOver".SceneId] = scene_renderTextRichOver.exportedScene - result["renderTextSplit".SceneId] = scene_renderTextSplit.exportedScene - result["sceneNodes".SceneId] = scene_sceneNodes.exportedScene + for spec in sceneSpecs: + let exportedScene = loadSharedScene(spec) + if exportedScene.isSome: + result[spec.id] = exportedScene.get() diff --git a/e2e/generated/shared/scene_black.nim b/e2e/generated/shared/scene_black.nim new file mode 100644 index 000000000..007737a1d --- /dev/null +++ b/e2e/generated/shared/scene_black.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_black as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_blue.nim b/e2e/generated/shared/scene_blue.nim new file mode 100644 index 000000000..d5b70f8f9 --- /dev/null +++ b/e2e/generated/shared/scene_blue.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_blue as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_dataCodeFloat.nim b/e2e/generated/shared/scene_dataCodeFloat.nim new file mode 100644 index 000000000..53eebcea8 --- /dev/null +++ b/e2e/generated/shared/scene_dataCodeFloat.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_dataCodeFloat as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_dataDownloadImage.nim b/e2e/generated/shared/scene_dataDownloadImage.nim new file mode 100644 index 000000000..aed8b12f2 --- /dev/null +++ b/e2e/generated/shared/scene_dataDownloadImage.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_dataDownloadImage as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_dataDownloadUrl.nim b/e2e/generated/shared/scene_dataDownloadUrl.nim new file mode 100644 index 000000000..256f4cf62 --- /dev/null +++ b/e2e/generated/shared/scene_dataDownloadUrl.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_dataDownloadUrl as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_dataGradient.nim b/e2e/generated/shared/scene_dataGradient.nim new file mode 100644 index 000000000..287179db0 --- /dev/null +++ b/e2e/generated/shared/scene_dataGradient.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_dataGradient as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_dataLocalImage.nim b/e2e/generated/shared/scene_dataLocalImage.nim new file mode 100644 index 000000000..7d020ab7e --- /dev/null +++ b/e2e/generated/shared/scene_dataLocalImage.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_dataLocalImage as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_dataNewImage.nim b/e2e/generated/shared/scene_dataNewImage.nim new file mode 100644 index 000000000..e3d900b29 --- /dev/null +++ b/e2e/generated/shared/scene_dataNewImage.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_dataNewImage as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_dataNewImageNext.nim b/e2e/generated/shared/scene_dataNewImageNext.nim new file mode 100644 index 000000000..f8488b9a2 --- /dev/null +++ b/e2e/generated/shared/scene_dataNewImageNext.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_dataNewImageNext as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_dataQR.nim b/e2e/generated/shared/scene_dataQR.nim new file mode 100644 index 000000000..2376be53a --- /dev/null +++ b/e2e/generated/shared/scene_dataQR.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_dataQR as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_dataResize.nim b/e2e/generated/shared/scene_dataResize.nim new file mode 100644 index 000000000..4b7a09471 --- /dev/null +++ b/e2e/generated/shared/scene_dataResize.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_dataResize as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_logicIfElse.nim b/e2e/generated/shared/scene_logicIfElse.nim new file mode 100644 index 000000000..3fccf60d2 --- /dev/null +++ b/e2e/generated/shared/scene_logicIfElse.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_logicIfElse as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_logicSetAsState.nim b/e2e/generated/shared/scene_logicSetAsState.nim new file mode 100644 index 000000000..970f86a60 --- /dev/null +++ b/e2e/generated/shared/scene_logicSetAsState.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_logicSetAsState as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_renderColorFlow.nim b/e2e/generated/shared/scene_renderColorFlow.nim new file mode 100644 index 000000000..c92807496 --- /dev/null +++ b/e2e/generated/shared/scene_renderColorFlow.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_renderColorFlow as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_renderColorImage.nim b/e2e/generated/shared/scene_renderColorImage.nim new file mode 100644 index 000000000..4e5c4b764 --- /dev/null +++ b/e2e/generated/shared/scene_renderColorImage.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_renderColorImage as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_renderColorSplit.nim b/e2e/generated/shared/scene_renderColorSplit.nim new file mode 100644 index 000000000..084909701 --- /dev/null +++ b/e2e/generated/shared/scene_renderColorSplit.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_renderColorSplit as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_renderGradientSplit.nim b/e2e/generated/shared/scene_renderGradientSplit.nim new file mode 100644 index 000000000..eb62f74ac --- /dev/null +++ b/e2e/generated/shared/scene_renderGradientSplit.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_renderGradientSplit as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_renderImage.nim b/e2e/generated/shared/scene_renderImage.nim new file mode 100644 index 000000000..d0d20081e --- /dev/null +++ b/e2e/generated/shared/scene_renderImage.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_renderImage as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_renderImageBlend.nim b/e2e/generated/shared/scene_renderImageBlend.nim new file mode 100644 index 000000000..d0c9596f0 --- /dev/null +++ b/e2e/generated/shared/scene_renderImageBlend.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_renderImageBlend as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_renderImageMask.nim b/e2e/generated/shared/scene_renderImageMask.nim new file mode 100644 index 000000000..87803d9c3 --- /dev/null +++ b/e2e/generated/shared/scene_renderImageMask.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_renderImageMask as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_renderOpacity.nim b/e2e/generated/shared/scene_renderOpacity.nim new file mode 100644 index 000000000..dcb1d2f83 --- /dev/null +++ b/e2e/generated/shared/scene_renderOpacity.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_renderOpacity as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_renderSplitData.nim b/e2e/generated/shared/scene_renderSplitData.nim new file mode 100644 index 000000000..31a79e171 --- /dev/null +++ b/e2e/generated/shared/scene_renderSplitData.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_renderSplitData as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_renderSplitFlow.nim b/e2e/generated/shared/scene_renderSplitFlow.nim new file mode 100644 index 000000000..83eec6817 --- /dev/null +++ b/e2e/generated/shared/scene_renderSplitFlow.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_renderSplitFlow as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_renderSplitLoop.nim b/e2e/generated/shared/scene_renderSplitLoop.nim new file mode 100644 index 000000000..d98bfac6e --- /dev/null +++ b/e2e/generated/shared/scene_renderSplitLoop.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_renderSplitLoop as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_renderTextOverflow.nim b/e2e/generated/shared/scene_renderTextOverflow.nim new file mode 100644 index 000000000..897ffb282 --- /dev/null +++ b/e2e/generated/shared/scene_renderTextOverflow.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_renderTextOverflow as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_renderTextPosition.nim b/e2e/generated/shared/scene_renderTextPosition.nim new file mode 100644 index 000000000..8eaf58ff4 --- /dev/null +++ b/e2e/generated/shared/scene_renderTextPosition.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_renderTextPosition as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_renderTextRich.nim b/e2e/generated/shared/scene_renderTextRich.nim new file mode 100644 index 000000000..d3bbc31d8 --- /dev/null +++ b/e2e/generated/shared/scene_renderTextRich.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_renderTextRich as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_renderTextRichOver.nim b/e2e/generated/shared/scene_renderTextRichOver.nim new file mode 100644 index 000000000..f92e522bf --- /dev/null +++ b/e2e/generated/shared/scene_renderTextRichOver.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_renderTextRichOver as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_renderTextSplit.nim b/e2e/generated/shared/scene_renderTextSplit.nim new file mode 100644 index 000000000..b6e6ea831 --- /dev/null +++ b/e2e/generated/shared/scene_renderTextSplit.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_renderTextSplit as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/generated/shared/scene_sceneNodes.nim b/e2e/generated/shared/scene_sceneNodes.nim new file mode 100644 index 000000000..e01cb3ef3 --- /dev/null +++ b/e2e/generated/shared/scene_sceneNodes.nim @@ -0,0 +1,11 @@ +# This file is autogenerated + +import scenes/scene_sceneNodes as sceneModule +import frameos/channels +import frameos/driver_abi + +proc frameos_scene_init*(logHook: HostLogProc, sendEventHook: HostSendEventProc) {.cdecl, exportc, dynlib.} = + setSharedHostCallbacks(logHook, sendEventHook) + +proc frameos_scene_export*(): pointer {.cdecl, exportc, dynlib.} = + result = cast[pointer](sceneModule.exportedScene) diff --git a/e2e/makescenes.py b/e2e/makescenes.py index f99628d8c..62570276e 100644 --- a/e2e/makescenes.py +++ b/e2e/makescenes.py @@ -3,7 +3,7 @@ import json from pathlib import Path -from app.codegen.scene_nim import write_scene_nim, write_scenes_nim +from app.codegen.scene_nim import scene_module_filename, write_scene_library_nim, write_scene_nim, write_scenes_nim from app.models import Frame if __name__ == '__main__': @@ -16,6 +16,11 @@ filter_str = filter_str.strip().lower() generated_dir.mkdir(exist_ok=True) + shared_generated_dir = generated_dir / 'shared' + if shared_generated_dir.exists(): + for file_path in shared_generated_dir.glob('*.nim'): + file_path.unlink() + shared_generated_dir.mkdir(exist_ok=True) files = sorted(scenes_dir.glob('*.json')) if filter_str: @@ -46,8 +51,11 @@ scene_file_path = generated_dir / f'scene_{scene_name}.nim' with open(scene_file_path, 'w') as scene_file: scene_file.write(scene_nim) + scene_library_path = shared_generated_dir / scene_module_filename(scene_data) + with open(scene_library_path, 'w') as scene_library_file: + scene_library_file.write(write_scene_library_nim(scene_data)) - scenes_nim = write_scenes_nim(frame) + scenes_nim = write_scenes_nim(frame, compilation_mode="shared") scenes_nim_file_path = generated_dir / 'scenes.nim' with open(scenes_nim_file_path, 'w') as scenes_file: scenes_file.write("# This file is autogenerated\n" + scenes_nim) diff --git a/frameos/Makefile b/frameos/Makefile index 6a80c4f90..486c132c8 100644 --- a/frameos/Makefile +++ b/frameos/Makefile @@ -1,11 +1,12 @@ -.PHONY: run build drivers maybe-driver-libraries driver-libraries test test-shard debug cross-list release-% release-all +.PHONY: run build drivers maybe-driver-libraries maybe-scene-libraries driver-libraries scene-libraries test test-shard debug cross-list release-% release-all CROSS_OUT ?= build/cross PREBUILT_OUT ?= build/prebuilt-cross -DRIVER_BUILD_MODE ?= +COMPILATION_MODE ?= RELEASE_ARGS ?= SKIP_DRIVER_LIBRARIES ?= 0 -DRIVER_BUILD_MODE_ARG = $(if $(DRIVER_BUILD_MODE),--driver-build-mode $(DRIVER_BUILD_MODE),) +SKIP_SCENE_LIBRARIES ?= 0 +COMPILATION_MODE_ARG = $(if $(COMPILATION_MODE),--compilation-mode $(COMPILATION_MODE),) NIM ?= nim NIMCACHE ?= .nimcache FRAMEOS_VERSION ?= $(shell python3 tools/frameos_version.py ../versions.json) @@ -14,7 +15,7 @@ NIM_BUILD_FLAGS = --out:build/frameos --nimcache:"$(NIMCACHE)" --lineTrace:on -- run: build ./build/frameos --verbose -build: apploaders drivers maybe-driver-libraries +build: apploaders drivers maybe-driver-libraries maybe-scene-libraries python3 tools/prepare_assets.py @if [ ! -d quickjs ]; then nimble build_quickjs --silent; fi @if [ ! -f nimble.paths ]; then nimble setup --silent; fi @@ -22,19 +23,29 @@ build: apploaders drivers maybe-driver-libraries $(NIM) c $(NIM_BUILD_FLAGS) src/frameos.nim drivers: - python3 tools/generate_driver_sources.py --config frame.json $(DRIVER_BUILD_MODE_ARG) + python3 tools/generate_driver_sources.py --config frame.json $(COMPILATION_MODE_ARG) maybe-driver-libraries: @if [ "$(SKIP_DRIVER_LIBRARIES)" = "1" ]; then \ echo "Skipping shared driver libraries"; \ else \ - python3 tools/build_driver_libraries.py --config frame.json --out build/drivers --only-if-shared $(DRIVER_BUILD_MODE_ARG); \ + python3 tools/build_driver_libraries.py --config frame.json --out build/drivers --only-if-shared $(COMPILATION_MODE_ARG); \ + fi + +maybe-scene-libraries: + @if [ "$(SKIP_SCENE_LIBRARIES)" = "1" ]; then \ + echo "Skipping shared scene libraries"; \ + else \ + python3 tools/build_scene_libraries.py --config frame.json --out build/scenes --only-if-shared $(COMPILATION_MODE_ARG); \ fi driver-libraries: drivers python3 tools/build_driver_libraries.py --config frame.json --out build/drivers -debug: apploaders drivers maybe-driver-libraries +scene-libraries: + python3 tools/build_scene_libraries.py --config frame.json --out build/scenes + +debug: apploaders drivers maybe-driver-libraries maybe-scene-libraries python3 tools/prepare_assets.py @if [ ! -d quickjs ]; then nimble build_quickjs --silent; fi @if [ ! -f nimble.paths ]; then nimble setup --silent; fi diff --git a/frameos/src/frameos/channels.nim b/frameos/src/frameos/channels.nim index 150c7c06d..108dc9c43 100644 --- a/frameos/src/frameos/channels.nim +++ b/frameos/src/frameos/channels.nim @@ -1,4 +1,4 @@ -when defined(frameosDriverLibrary): +when defined(frameosDriverLibrary) or defined(frameosSharedLibrary): import json import options import frameos/ids diff --git a/frameos/tools/build_driver_libraries.py b/frameos/tools/build_driver_libraries.py index f5885ad57..71d2f9f48 100644 --- a/frameos/tools/build_driver_libraries.py +++ b/frameos/tools/build_driver_libraries.py @@ -21,12 +21,12 @@ sys.path.insert(0, str(BACKEND_ROOT)) from app.codegen.drivers_nim import ( # noqa: E402 - DRIVER_BUILD_MODE_SHARED, + COMPILATION_MODE_SHARED, compiled_drivers, - driver_build_mode_uses_shared_libraries, + compilation_mode_uses_shared_libraries, driver_library_filename, - frame_driver_build_mode, - normalize_driver_build_mode, + frame_compilation_mode, + normalize_compilation_mode, ) from app.drivers.devices import drivers_for_frame # noqa: E402 from generate_driver_sources import generate_driver_sources, load_frame_stub # noqa: E402 @@ -68,12 +68,12 @@ async def build_driver_libraries( nim_args: Iterable[str], strip_command: str | None, only_if_shared: bool, - driver_build_mode: str | None, + compilation_mode: str | None, ) -> list[Path]: frame = load_frame_stub(config_path) - mode = normalize_driver_build_mode(driver_build_mode or frame_driver_build_mode(frame)) - if only_if_shared and not driver_build_mode_uses_shared_libraries(mode): - print(f"Driver build mode is {mode}; skipping shared driver libraries") + mode = normalize_compilation_mode(compilation_mode or frame_compilation_mode(frame)) + if only_if_shared and not compilation_mode_uses_shared_libraries(mode): + print(f"Compilation mode is {mode}; skipping shared driver libraries") return [] out_dir.mkdir(parents=True, exist_ok=True) @@ -88,7 +88,7 @@ async def build_driver_libraries( generate_driver_sources( frameos_root=source_dir, config_path=config_path, - driver_build_mode=DRIVER_BUILD_MODE_SHARED, + compilation_mode=COMPILATION_MODE_SHARED, ) if not (source_dir / "quickjs" / "libquickjs.a").exists(): @@ -131,13 +131,13 @@ def parse_args(argv: list[str]) -> argparse.Namespace: parser.add_argument( "--only-if-shared", action="store_true", - help="Skip unless the effective driver build mode uses shared libraries", + help="Skip unless the effective compilation mode uses shared libraries", ) parser.add_argument( - "--driver-build-mode", + "--compilation-mode", choices=("static", "shared", "precompiled"), default=None, - help="Override frame.json rpios.driverBuildMode when deciding whether to skip", + help="Override frame.json rpios.compilationMode when deciding whether to skip", ) parser.add_argument( "--nim-arg", @@ -159,7 +159,7 @@ def main(argv: list[str]) -> int: nim_args=args.nim_arg, strip_command=None if args.no_strip else args.strip, only_if_shared=args.only_if_shared, - driver_build_mode=args.driver_build_mode, + compilation_mode=args.compilation_mode, ) ) for path in built: diff --git a/frameos/tools/build_scene_libraries.py b/frameos/tools/build_scene_libraries.py new file mode 100644 index 000000000..0d3019baa --- /dev/null +++ b/frameos/tools/build_scene_libraries.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""Build configured FrameOS compiled scenes as shared libraries.""" +from __future__ import annotations + +import argparse +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path +from typing import Iterable + + +FRAMEOS_ROOT = Path(__file__).resolve().parents[1] +REPO_ROOT = FRAMEOS_ROOT.parent +BACKEND_ROOT = REPO_ROOT / "backend" +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) +if str(BACKEND_ROOT) not in sys.path: + sys.path.insert(0, str(BACKEND_ROOT)) + +from app.codegen.drivers_nim import ( # noqa: E402 + compilation_mode_uses_shared_libraries, + frame_compilation_mode, + normalize_compilation_mode, +) +from generate_driver_sources import load_frame_stub # noqa: E402 + +LINUX_SIZE_FLAGS = [ + "--opt:size", + "--stackTrace:off", + "--lineTrace:off", + "--passC:-ffunction-sections", + "--passC:-fdata-sections", + "--passC:-fno-asynchronous-unwind-tables", + "--passC:-fno-unwind-tables", + "--passL:-Wl,--gc-sections", +] + + +def run(command: list[str], *, cwd: Path) -> None: + print("> " + " ".join(command)) + subprocess.run(command, cwd=cwd, check=True) + + +def strip_library(path: Path, strip_command: str | None) -> None: + if not strip_command: + return + command = [strip_command, "--strip-unneeded", str(path)] + print("> " + " ".join(command)) + try: + subprocess.run(command, check=True) + except (FileNotFoundError, subprocess.CalledProcessError) as err: + print(f"Skipping strip for {path}: {err}", file=sys.stderr) + + +def build_scene_libraries( + *, + frameos_root: Path, + config_path: Path, + out_dir: Path, + nim_path: str, + nim_args: Iterable[str], + strip_command: str | None, + only_if_shared: bool, + compilation_mode: str | None, +) -> list[Path]: + frame = load_frame_stub(config_path) + mode = normalize_compilation_mode(compilation_mode or frame_compilation_mode(frame)) + if only_if_shared and not compilation_mode_uses_shared_libraries(mode): + print(f"Compilation mode is {mode}; skipping shared scene libraries") + return [] + + shared_scene_sources = sorted((frameos_root / "src" / "scenes" / "shared").glob("*.nim")) + if not shared_scene_sources: + print("No shared scene sources found; skipping shared scene libraries") + return [] + + out_dir.mkdir(parents=True, exist_ok=True) + + with tempfile.TemporaryDirectory(prefix="frameos-scene-libs-") as tmp: + source_dir = Path(tmp) / "frameos" + shutil.copytree( + frameos_root, + source_dir, + ignore=shutil.ignore_patterns("build", "node_modules"), + ) + + if not (source_dir / "quickjs" / "libquickjs.a").exists(): + run(["nimble", "build_quickjs", "--silent"], cwd=source_dir) + run(["nimble", "assets", "-y"], cwd=source_dir) + run(["nimble", "setup"], cwd=source_dir) + + built: list[Path] = [] + for source_path in shared_scene_sources: + source_name = source_path.name + library_name = source_path.with_suffix(".so").name + output = out_dir / library_name + nimcache = out_dir / ".nimcache" / source_path.stem + if nimcache.exists(): + shutil.rmtree(nimcache) + command = [ + nim_path, + "compile", + "--app:lib", + "--define:frameosSharedLibrary", + *(LINUX_SIZE_FLAGS if sys.platform.startswith("linux") else ["--opt:size"]), + f"--nimcache:{nimcache}", + f"--out:{output}", + *list(nim_args), + f"src/scenes/shared/{source_name}", + ] + run(command, cwd=source_dir) + strip_library(output, strip_command) + built.append(output) + + return built + + +def parse_args(argv: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--frameos-root", default=str(FRAMEOS_ROOT), help="Path to the frameos source tree") + parser.add_argument("--config", default=str(FRAMEOS_ROOT / "frame.json"), help="Frame config JSON") + parser.add_argument("--out", default=str(FRAMEOS_ROOT / "build" / "scenes"), help="Output directory for .so files") + parser.add_argument("--nim", default="nim", help="Nim compiler executable") + parser.add_argument("--strip", default="strip", help="Strip executable to run on built libraries") + parser.add_argument("--no-strip", action="store_true", help="Do not strip built libraries") + parser.add_argument( + "--only-if-shared", + action="store_true", + help="Skip unless the effective compilation mode uses shared libraries", + ) + parser.add_argument( + "--compilation-mode", + choices=("static", "shared", "precompiled"), + default=None, + help="Override frame.json rpios.compilationMode when deciding whether to skip", + ) + parser.add_argument( + "--nim-arg", + action="append", + default=[], + help="Extra argument to pass to nim compile; repeat for multiple arguments", + ) + return parser.parse_args(argv) + + +def main(argv: list[str]) -> int: + args = parse_args(argv) + built = build_scene_libraries( + frameos_root=Path(args.frameos_root).resolve(), + config_path=Path(args.config).resolve(), + out_dir=Path(args.out).resolve(), + nim_path=args.nim, + nim_args=args.nim_arg, + strip_command=None if args.no_strip else args.strip, + only_if_shared=args.only_if_shared, + compilation_mode=args.compilation_mode, + ) + for path in built: + print(f"Built {path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/frameos/tools/generate_driver_sources.py b/frameos/tools/generate_driver_sources.py index ed726db97..24c6cc8f9 100644 --- a/frameos/tools/generate_driver_sources.py +++ b/frameos/tools/generate_driver_sources.py @@ -21,9 +21,9 @@ from app.codegen.drivers_nim import ( # noqa: E402 compiled_drivers, - driver_build_mode_uses_shared_libraries, - frame_driver_build_mode, - normalize_driver_build_mode, + compilation_mode_uses_shared_libraries, + frame_compilation_mode, + normalize_compilation_mode, write_driver_library_nim, write_drivers_nim, ) @@ -66,15 +66,15 @@ def generate_driver_sources( *, frameos_root: Path, config_path: Path, - driver_build_mode: str | None, + compilation_mode: str | None, ) -> str: frame = load_frame_stub(config_path) - mode = normalize_driver_build_mode(driver_build_mode or frame_driver_build_mode(frame)) + mode = normalize_compilation_mode(compilation_mode or frame_compilation_mode(frame)) drivers = drivers_for_frame(frame) drivers_dir = frameos_root / "src" / "drivers" (drivers_dir / "drivers.nim").write_text( - write_drivers_nim(drivers, driver_build_mode=mode), + write_drivers_nim(drivers, compilation_mode=mode), encoding="utf-8", ) @@ -86,7 +86,7 @@ def generate_driver_sources( shared_dir = drivers_dir / "shared" shutil.rmtree(shared_dir, ignore_errors=True) - if driver_build_mode_uses_shared_libraries(mode): + if compilation_mode_uses_shared_libraries(mode): shared_dir.mkdir(parents=True, exist_ok=True) for driver in compiled_drivers(drivers): (shared_dir / f"{driver.name}.nim").write_text( @@ -102,10 +102,10 @@ def parse_args(argv: list[str]) -> argparse.Namespace: parser.add_argument("--frameos-root", default=str(FRAMEOS_ROOT), help="Path to the frameos source tree") parser.add_argument("--config", default=str(FRAMEOS_ROOT / "frame.json"), help="Frame config JSON") parser.add_argument( - "--driver-build-mode", + "--compilation-mode", choices=("static", "shared", "precompiled"), default=None, - help="Override frame.json rpios.driverBuildMode", + help="Override frame.json rpios.compilationMode", ) return parser.parse_args(argv) @@ -115,7 +115,7 @@ def main(argv: list[str]) -> int: mode = generate_driver_sources( frameos_root=Path(args.frameos_root).resolve(), config_path=Path(args.config).resolve(), - driver_build_mode=args.driver_build_mode, + compilation_mode=args.compilation_mode, ) print(f"Generated driver sources in {mode} mode") return 0 diff --git a/frontend/src/scenes/frame/frameDeployUtils.ts b/frontend/src/scenes/frame/frameDeployUtils.ts index 67df7e441..81c619f13 100644 --- a/frontend/src/scenes/frame/frameDeployUtils.ts +++ b/frontend/src/scenes/frame/frameDeployUtils.ts @@ -27,7 +27,7 @@ export interface FullDeployPlanResponse { low_memory: boolean drivers: string[] binary: { - driver_build_mode?: 'static' | 'shared' | 'precompiled' + compilation_mode?: 'static' | 'shared' | 'precompiled' will_attempt_cross_compile?: boolean will_attempt_precompiled?: boolean cross_compile_supported?: boolean @@ -163,15 +163,15 @@ export function buildFullDeployPlanSummary( if (fullPlan.drivers.length > 0) { items.push({ label: 'Drivers', value: stringifyList(fullPlan.drivers) }) } - if (fullPlan.binary.driver_build_mode === 'shared') { - items.push({ label: 'Driver delivery', value: 'Shared libraries deployed next to the FrameOS binary' }) + if (fullPlan.binary.compilation_mode === 'shared') { + items.push({ label: 'Compilation', value: 'Shared libraries deployed next to the FrameOS binary' }) } - if (fullPlan.binary.driver_build_mode === 'precompiled') { + if (fullPlan.binary.compilation_mode === 'precompiled') { items.push({ - label: 'Driver delivery', + label: 'Compilation', value: fullPlan.binary.will_attempt_precompiled ? 'Precompiled FrameOS binary and shared driver libraries' - : `Shared driver libraries; precompiled release skipped${ + : `Shared libraries; precompiled release skipped${ fullPlan.binary.precompiled_skip_reason ? ` (${fullPlan.binary.precompiled_skip_reason})` : '' }`, }) diff --git a/frontend/src/scenes/frame/frameLogic.ts b/frontend/src/scenes/frame/frameLogic.ts index 83b18edd8..a1d5fbcc3 100644 --- a/frontend/src/scenes/frame/frameLogic.ts +++ b/frontend/src/scenes/frame/frameLogic.ts @@ -561,9 +561,16 @@ function hasValidPosition(node: DiagramNode): boolean { function sanitizeFrame(frame: Partial): Partial { const frameAdminAuthUser = frame.frame_admin_auth?.user ?? '' const frameAdminAuthPass = frame.frame_admin_auth?.pass ?? '' + const rpios = frame.rpios + ? { + ...frame.rpios, + compilationMode: frame.rpios.compilationMode ?? '', + } + : frame.rpios return { ...frame, + rpios, frame_admin_auth: { enabled: frame.frame_admin_auth?.enabled ?? false, user: frameAdminAuthUser, @@ -905,7 +912,13 @@ export const frameLogic = kea([ ], requiresRecompilation: [ (s) => [s.lastDeploy, s.frame, s.frameForm, s.mode, s.isFrameAdminMode], - (lastDeploy, frame, frameForm, mode, isFrameAdminMode): boolean => { + ( + lastDeploy: Partial | null, + frame: FrameType, + frameForm: Partial, + mode: FrameType['mode'], + isFrameAdminMode: boolean + ): boolean => { if (isFrameAdminMode) { return false } @@ -920,7 +933,13 @@ export const frameLogic = kea([ ], undeployedSummaryItems: [ (s) => [s.lastDeploy, s.frame, s.frameForm, s.requiresRecompilation, s.isFrameAdminMode], - (lastDeploy, frame, frameForm, requiresRecompilation, isFrameAdminMode): SummaryItem[] => { + ( + lastDeploy: Partial | null, + frame: FrameType, + frameForm: Partial, + requiresRecompilation: boolean, + isFrameAdminMode: boolean + ): SummaryItem[] => { const pendingFrame = Object.keys(frameForm ?? {}).length > 0 ? frameForm : frame return isFrameAdminMode ? [] : buildUndeployedSummaryItems(lastDeploy, pendingFrame, requiresRecompilation) }, diff --git a/frontend/src/scenes/frame/panels/FrameSettings/FrameSettings.tsx b/frontend/src/scenes/frame/panels/FrameSettings/FrameSettings.tsx index dbf6aa430..f4571e733 100644 --- a/frontend/src/scenes/frame/panels/FrameSettings/FrameSettings.tsx +++ b/frontend/src/scenes/frame/panels/FrameSettings/FrameSettings.tsx @@ -538,26 +538,26 @@ export function FrameSettings({ className, hideDropdown, hideDeploymentMode }: F />

- Choose whether display/input drivers are built as separate shared libraries deployed next to - FrameOS, or linked into the FrameOS executable. + Choose whether display/input drivers and compiled scenes are built as separate shared libraries + deployed next to FrameOS, or linked into the FrameOS executable.

Precompiled only downloads a published FrameOS release when all scenes are interpreted; otherwise - it falls back to shared driver libraries. + it falls back to shared libraries.

} >