Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions mosaico-tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,3 +352,24 @@ Run upload-specific unit tests with:
```sh
python3 -m unittest discover -s mosaico-tools/tests -v
```


## Raylib games with Iris

`mosaico.py game build sky_hop --target iris` builds an Engine example;
an independent native game project path is accepted by the same command.
The selected Engine checkout must provide the Mosaico Board Manager profile
under `examples/boards/esp-mosaico/bmgr/esp_mosaico`.

The CLI resolves the Board Manager IDF action in a tool-owned bootstrap project,
then generates `components/gen_bmgr_codes` inside the Iris wrapper. Users do not
need to install `esp-bmgr-assist` or run BMGR manually for an Iris game build.
Generated hardware configuration in the original game project is excluded, so a
native BOX-3 selection cannot replace the Iris wrapper's Mosaico hardware.
Game source files are not modified. A separate wrapper cache is used for BMGR.

The Engine Board initializes game hardware. Utils supplies Iris USB services,
screenshot/input registration and health confirmation after the accepted first
frame. The wrapper retains the product Recovery partition table and required
Iris settings. Raylib game builds no longer inject the product BSP as a game
Board. Ordinary Iris applications and retained Recovery keep their own builds.
16 changes: 12 additions & 4 deletions mosaico-tools/cmake/raylib_lite_iris_app.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# The project directory must contain templates/blank_game/partitions.csv.
# `mosaico.py game build --target iris <game>` generates such a project.
# RAYLIB_LITE_ENGINE_ROOT and MOSAICO_BSP_ROOT are CMake or environment inputs.
# RAYLIB_LITE_ENGINE_ROOT selects the hardware Board; Iris services live in utils.
include_guard(GLOBAL)

get_filename_component(_raylib_iris_tools "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
Expand Down Expand Up @@ -65,13 +65,21 @@ list(APPEND EXTRA_COMPONENT_DIRS
"${_raylib_iris_tools}/components/esp_mosaico_raylib_iris"
"${_raylib_iris_utils}/ESP-Iris/components/esp_iris"
"${_raylib_iris_utils}/esp-mosaico-recovery/components/esp_mosaico_app_recovery")
if(NOT RAYLIB_LITE_BSP_DIR AND DEFINED ENV{MOSAICO_BSP_ROOT})
set(RAYLIB_LITE_BSP_DIR "$ENV{MOSAICO_BSP_ROOT}")
if(NOT EXISTS "${CMAKE_SOURCE_DIR}/components/gen_bmgr_codes/gen_board_metadata.yaml")
message(FATAL_ERROR "Generate the wrapper Board with mosaico.py game build --target iris")
endif()
include("${RAYLIB_LITE_ENGINE_ROOT}/examples/common_components/examples_common/project.cmake")
list(APPEND EXTRA_COMPONENT_DIRS "${_raylib_iris_game}/main")
if(EXISTS "${_raylib_iris_game}/components")
list(APPEND EXTRA_COMPONENT_DIRS "${_raylib_iris_game}/components")
# User components remain available, but native BMGR output belongs to
# that native project and must not override the wrapper's Mosaico profile.
file(GLOB _raylib_iris_components LIST_DIRECTORIES true "${_raylib_iris_game}/components/*")
foreach(_component IN LISTS _raylib_iris_components)
get_filename_component(_name "${_component}" NAME)
if(NOT _name STREQUAL "gen_bmgr_codes" AND EXISTS "${_component}/CMakeLists.txt")
list(APPEND EXTRA_COMPONENT_DIRS "${_component}")
endif()
endforeach()
endif()
set(MOSAICO_ESP_IRIS_ROOT "${_raylib_iris_utils}/ESP-Iris")
set(MOSAICO_SYSTEM_UPDATE_CMAKE "${_raylib_iris_tools}/cmake/system_update.cmake")
Expand Down
52 changes: 52 additions & 0 deletions mosaico-tools/tests/test_raylib_bmgr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Iris wrappers own their BMGR output independently of native Game projects."""
from pathlib import Path
from types import SimpleNamespace
import sys
from unittest.mock import patch
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
from mosaico_cli.app_commands import _prepare_iris_board
from mosaico_cli.errors import BuildError, EnvironmentError

@pytest.mark.parametrize('generated_board', ['esp_mosaico', 'esp32_s3_box_3'])
def test_wrapper_board_is_generated_and_verified(tmp_path, generated_board):
engine = tmp_path / 'engine'
profile = engine / 'examples/boards/esp-mosaico/bmgr/esp_mosaico'
profile.mkdir(parents=True)
(profile / 'board_info.yaml').write_text('board: esp_mosaico\n')
project = tmp_path / 'runs/my-game/project'
project.mkdir(parents=True)
native = tmp_path / 'user-game/components/gen_bmgr_codes'
native.mkdir(parents=True)
(native / 'gen_board_metadata.yaml').write_text('board: esp32_s3_box_3\n')
commands = []
def resolve_plugin(context, **kwargs):
plugin = kwargs['project'] / 'managed_components/espressif__esp_board_manager'
plugin.mkdir(parents=True)
(plugin / 'idf_ext.py').write_text('')
def generate(argv, **kwargs):
commands.append((argv, kwargs))
output = project / 'components/gen_bmgr_codes'
output.mkdir(parents=True)
(output / 'gen_board_metadata.yaml').write_text('board: '+generated_board+'\n')
return SimpleNamespace(returncode=0)
context = SimpleNamespace(workspace=SimpleNamespace(), run=generate, log_path=tmp_path/'log')
with patch('mosaico_cli.runtime.resolve_idf_path', return_value=tmp_path/'idf'), patch(
'mosaico_cli.runtime.run_idf_target', side_effect=resolve_plugin), patch(
'mosaico_cli.runtime.idf_target_command', return_value={'argv':['idf.py','bmgr'], 'env':{}}):
if generated_board == 'esp_mosaico':
_prepare_iris_board(context, project, engine)
else:
with pytest.raises(BuildError, match='required Mosaico'):
_prepare_iris_board(context, project, engine)
assert commands[0][0][-4:] == ['-c', str(profile.parent), '-b', 'esp_mosaico']
assert commands[0][1]['cwd'] == project
assert 'IDF_EXTRA_ACTIONS_PATH' in commands[0][1]['env']
assert (native/'gen_board_metadata.yaml').read_text() == 'board: esp32_s3_box_3\n'

def test_missing_profile_fails_before_idf_execution(tmp_path):
context = SimpleNamespace(workspace=SimpleNamespace())
with patch('mosaico_cli.runtime.run_idf_target') as run:
with pytest.raises(EnvironmentError, match='BMGR profile'):
_prepare_iris_board(context, tmp_path/'wrapper', tmp_path/'engine')
run.assert_not_called()
53 changes: 50 additions & 3 deletions mosaico-tools/tools/mosaico_cli/app_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def _iris_project(workspace, selected):
template = workspace.tool_root / "templates" / "blank_game"
# Source paths, including named examples in another Engine checkout, must
# never reuse each other's component-manager lock or build cache.
key = name + "-" + hashlib.sha256(str(source).encode()).hexdigest()[:12]
key = name + "-bmgr-" + hashlib.sha256(str(source).encode()).hexdigest()[:12]
project = workspace.run_dir / "raylib-iris" / key / "project"
project.mkdir(parents=True, exist_ok=True)
(project / "CMakeLists.txt").write_text(IRIS_PROJECT_CMAKE.format(
Expand All @@ -90,6 +90,48 @@ def _iris_project(workspace, selected):
return project



def _prepare_iris_board(context, project, engine):
"""Generate hardware configuration inside the wrapper, never the Game."""
from .runtime import idf_target_command, resolve_idf_path, run_idf_target
from .errors import BuildError
profile = engine / "examples/boards/esp-mosaico/bmgr"
if not (profile / "esp_mosaico/board_info.yaml").is_file():
raise EnvironmentError("Raylib Iris builds require the Engine's Mosaico BMGR profile; update the Engine checkout.")
# Resolve the action plugin in a small tool-owned IDF project. This avoids
# configuring the real wrapper before gen_bmgr_codes exists, or depending
# on esp-bmgr-assist being installed in the user's shell environment.
bootstrap = project.parent / "bmgr-bootstrap"
main = bootstrap / "main"
main.mkdir(parents=True, exist_ok=True)
(bootstrap / "CMakeLists.txt").write_text(
'cmake_minimum_required(VERSION 3.16)\n'
'include($ENV{IDF_PATH}/tools/cmake/project.cmake)\n'
'project(raylib_bmgr_bootstrap)\n', encoding="utf-8")
(main / "CMakeLists.txt").write_text('idf_component_register()\n', encoding="utf-8")
(main / "idf_component.yml").write_text(
'dependencies:\n idf: ">=6.2"\n espressif/esp_board_manager: "^0.7.1"\n', encoding="utf-8")
(bootstrap / "sdkconfig.defaults").write_text(
'CONFIG_IDF_TARGET="esp32s31"\nCONFIG_IDF_EXPERIMENTAL_FEATURES=y\n', encoding="utf-8")
idf_path = resolve_idf_path(context.workspace, project)
run_idf_target(context, idf_path=idf_path, project=bootstrap,
build_dir=bootstrap / "build", target="reconfigure",
definitions={"IDF_TARGET": "esp32s31"}, timeout=900)
plugin = bootstrap / "managed_components/espressif__esp_board_manager"
if not (plugin / "idf_ext.py").is_file():
raise BuildError("The resolved Board Manager IDF action plugin is missing.")
command = idf_target_command(context, idf_path=idf_path, project=project,
build_dir=project / "build", target="bmgr", timeout=900)
command["env"]["IDF_EXTRA_ACTIONS_PATH"] = str(plugin)
command["argv"].extend(["-c", str(profile), "-b", "esp_mosaico"])
result = context.run(command["argv"], cwd=project, env=command["env"], timeout=900)
if result.returncode:
raise BuildError("Mosaico BMGR configuration generation failed.", details={"log": str(context.log_path)})
metadata = project / "components/gen_bmgr_codes/gen_board_metadata.yaml"
if not metadata.is_file() or not re.search(r"^board:\s*esp_mosaico\s*$", metadata.read_text(), re.MULTILINE):
raise BuildError("BMGR did not generate the required Mosaico Board configuration.")


def add_commands(commands, project_commands):
preview = project_commands.add_parser("sim", help="Preview a GSP application with its native PC backend")
preview.set_defaults(command="sim", public_command="project sim")
Expand Down Expand Up @@ -196,10 +238,15 @@ def run(arguments, workspace):
os.environ["IDF_PATH"] = str(Path(arguments.idf_path).expanduser().resolve())
os.environ["RAYLIB_LITE_ENGINE_ROOT"] = str(workspace.raylib_path)
os.environ["MOSAICO_UTILS_ROOT"] = str(workspace.tool_root.parent)
os.environ["MOSAICO_BSP_ROOT"] = str(workspace.bsp_path)
os.environ["RAYLIB_LITE_BSP_DIR"] = str(workspace.bsp_path)
if iris:
os.environ.pop("MOSAICO_BSP_ROOT", None)
os.environ.pop("RAYLIB_LITE_BSP_DIR", None)
else:
os.environ["MOSAICO_BSP_ROOT"] = str(workspace.bsp_path)
os.environ["RAYLIB_LITE_UTILS_DIR"] = str(workspace.tool_root.parent)
context = RunContext(workspace, "game-build", arguments.verbose, arguments.json)
if iris:
_prepare_iris_board(context, project, workspace.raylib_path)
build_application(context, project)
if iris:
from .gateway import ensure_iris_tools
Expand Down