diff --git a/.github/cspell.json b/.github/cspell.json index 91833ce..6d5023d 100644 --- a/.github/cspell.json +++ b/.github/cspell.json @@ -38,6 +38,7 @@ "libglib", "libgstreamer", "libinput", + "libnl", "libpipewire", "libsecret", "libsystemd", diff --git a/README.md b/README.md index b3545ec..9a38fd1 100644 --- a/README.md +++ b/README.md @@ -589,6 +589,54 @@ emb cross . --target local # …the same, explicit (or --target host) emb cross . --target rpi5 --build ``` +#### Layered manifests: `extends` (board → project → app) + +emb ships a **board library** — hardware/OS definitions for known boards under +its own `boards/` directory (e.g. `rpi5-bookworm`: triple, toolchain version, +sysroot image, partition, cpu tuning, the base GUI stack). A manifest pulls one +in with `cross.extends` so a project never re-spells hardware facts: + +```yaml +# ivi-homescreen/.emb/raspberry-pi.emb.yaml — the PROJECT layer +cross: + targets: + rpi5-bookworm: + extends: rpi5-bookworm # ← emb board (hardware/OS) + backends: { drm-kms-egl: { BUILD_BACKEND_DRM_KMS_EGL: 'ON' } } + augment: [ { pkg: libdisplay-info, min: "0.2.0", build: meson } ] + sysroot: { dev_packages: [ libgstreamer1.0-dev ] } # added to the board's +``` + +`extends` takes two forms: + +| Form | Resolves to | +|---|---| +| `` | a target in emb's board library (the **hardware** layer) | +| `#` | a target in another emb project at `` (the **project** layer) | + +The second form lets an **app** build on a project (e.g. ivi-homescreen) and add +only what is app-specific — typically **plugin configuration**, which depends on +the plugins the app ships, not on the embedder or the board: + +```yaml +# my-flutter-app/.emb/app.emb.yaml — the APP layer +# (ivi-homescreen checked out alongside the app) +cross: + targets: + rpi5-bookworm: + extends: '../ivi-homescreen#rpi5-bookworm' # ← project target (→ board) + defines: { DISABLE_PLUGINS: 'OFF', PLUGINS_DIR: '../ivi-homescreen-plugins/plugins' } + sysroot: { dev_packages: [ libnl-3-dev ] } +``` + +The chain collapses **app ⊕ project ⊕ board** with the same merge rules at every +layer: nested maps deep-merge; `cpu_flags` and `backends` **replace** (each is a +complete statement); `sysroot.dev_packages` **union** (each layer adds to the +stack below it). `` is resolved relative to the extending manifest's project +root (the `.emb/` parent, else the file's directory); cross-project reference +cycles are rejected. The board library location can be overridden with +`EMB_BOARDS_DIR` (it otherwise ships with emb). + #### Reproducible builds (`emb.lock`) A cross resolve pins what it actually used to **`emb.lock`** at the project root, diff --git a/examples/cross/app-extends.emb.yaml b/examples/cross/app-extends.emb.yaml new file mode 100644 index 0000000..6b57677 --- /dev/null +++ b/examples/cross/app-extends.emb.yaml @@ -0,0 +1,25 @@ +# App layer demo — a Flutter app that builds on a project (board → project → app). +# +# `extends: #` pulls in raspberry-pi-family.emb.yaml's rpi5-bookworm +# target — which itself `extends` the rpi5-bookworm board — then overlays only +# what is app-specific. Here that's one extra plugin -dev package, merged onto +# the project + board sysroot stack; a real app would typically also set its own +# plugin config (DISABLE_PLUGINS / PLUGINS_DIR). Hardware (toolchain, image, cpu) +# comes from the board; backends/augment from the project. +# +# emb cross app-extends.emb.yaml --target rpi5-bookworm --dry-run +# +# In a real layout the app and the project live in separate repos checked out +# side by side, so the ref is a directory, e.g.: +# extends: '../ivi-homescreen#rpi5-bookworm' +id: my-flutter-app +type: app +supported_archs: [arm64] +supported_host_types: [ubuntu, fedora] + +cross: + targets: + rpi5-bookworm: + extends: 'raspberry-pi-family.emb.yaml#rpi5-bookworm' + sysroot: + dev_packages: [libnl-3-dev] # an app plugin dep, added to the stack diff --git a/test/src/commands/cross_command_test.dart b/test/src/commands/cross_command_test.dart index bbe3a93..bfec31f 100644 --- a/test/src/commands/cross_command_test.dart +++ b/test/src/commands/cross_command_test.dart @@ -326,6 +326,21 @@ void main() { } }); + test( + '--dry-run plans the app-extends example (app → project → board)', + () async { + final app = p.join('examples', 'cross', 'app-extends.emb.yaml'); + final code = await run([ + 'cross', + '--dry-run', + '--target', + 'rpi5-bookworm', + app, + ]); + expect(code, ExitCode.success.code); + }, + ); + // Every shipped example must parse -> dispatch -> plan with no side effects: // this validates all of the listed use cases hermetically. test('--dry-run plans every example manifest', () async {