feat(cross): run native --target local bundle with --run
emb — Flutter Embedder CLI
emb provisions a Flutter embedded-Linux development workspace and builds
deployable app bundles for embedders such as
ivi-homescreen. It's a Dart
port of meta-flutter/workspace-automation
(flutter_workspace.py + create_aot.py).
The whole flow is a handful of commands:
deps → repos → Flutter SDK → engine → AOT → ivi-homescreen bundle
- Host dependency install in one transaction (PackageKit on Linux), with
WhatProvidesresolution sopkg-config,libjpeg-devel, etc. just work. - Prebuilt Flutter engine fetched from meta-flutter/flutter-engine releases (auto, keyed by the SDK's engine commit).
- Cross-compile AOT for
arm64/riscv64from anx86_64host using the engine's simulatorgen_snapshot— no qemu (the artifact is self-contained). - Self-describing packages: a package declares its build in a manifest and
emb build <dir>does the rest.
Requirements
- Dart SDK ≥ 3.10.1 to run/build
emb— or none preinstalled:bootstrap.shfetches a pinned SDK (see Install). - Linux for host dependency install (PackageKit — dnf/apt/zypper). The macOS (Homebrew) and Windows (WinGet) backends are stubbed in this build; all other commands are cross-platform.
git,tar,curlonPATH.- Cross-compiling to a device arch is supported from an x86_64 host.
Install
Quickest path — no preinstalled Dart or Flutter needed. bootstrap.sh
fetches a pinned Dart SDK and activates emb; --shellenv prints the exports
to put both on your PATH for the current shell:
git clone https://github.com/toyota-connected/emb_cli.git && cd emb_cli && eval "$(./bootstrap.sh --shellenv)" && emb --version
If you already have the Dart SDK, install from the checkout instead:
# From the package root:
dart pub get
dart pub global activate --source=path . # puts `emb` on PATH
emb --help
Make sure the pub-global bin dir ($PUB_CACHE/bin, e.g. ~/.pub-cache/bin) is
on PATH. You can also run without activating:
dart run bin/emb.dart <command> # from the package root
The Linux backend loads
packagekit_dart's native bridge (libpackagekit_nc.so).emblocates it automatically — from the package's own build, or from thepackage:hooksbuild-hook output under.dart_tool/. Override withPK_NC_LIB=/path/to/libpackagekit_nc.so.
Build ivi-homescreen locally
The native C/C++ embedder builds on this host — no Flutter, no cross
toolchain, just host dev libraries (EGL, GLES, Wayland, DRM/GBM, libinput,
xkbcommon, …; the example/ivi-homescreen
manifest declares the set per distro, and
examples/cross/ lists the full per-backend deps). It uses
the built-in local target (host compiler + system libraries).
# 1. Install host build deps (PackageKit on Linux, brew on macOS).
emb deps --packages example --yes # or --dry-run to preview
# 2. Build the embedder natively. `emb cross <file>` uses the file's parent as
# the CMake source, so drop the backend-matrix manifest next to the source.
git clone https://github.com/toyota-connected/ivi-homescreen.git
cp examples/cross/all-backends.emb.yaml ivi-homescreen/
emb cross ivi-homescreen/all-backends.emb.yaml --target local --build
# one backend only: --backend wayland-egl
Each backend lands at build-<backend>/shell/homescreen. Validated on Fedora —
all six backends build to distinct host (x86-64) ELF binaries:
| backend | backend | ||
|---|---|---|---|
wayland-egl |
✅ | drm-kms-vulkan |
✅ |
wayland-vulkan |
✅ | software |
✅ |
drm-kms-egl |
✅ | headless-egl |
✅ |
(Vulkan backends additionally need the Vulkan loader/headers; DRM backends need
libdisplay-info ≥ 0.2.0, libseat, libxcursor.) Cross-compile the same backends
for a device with --target <board> — see examples/cross/.
Quick start
# 0. Check host detection + package backend
emb doctor
# 1. Provision the workspace (host deps + repos + Flutter SDK + engine)
emb setup --config ../configs --yes
# 2. Load the environment it wrote (Flutter/Dart on PATH, FLUTTER_WORKSPACE, …)
. ./setup_env.sh
# 3. Build an ivi-homescreen bundle for a Raspberry Pi (arm64), release AOT
emb bundle --app-path ./app/my_app --arch arm64 --build
# 4. Run it on the target
# ivi-homescreen -b <workspace>/bundle/my_app-release-arm64
emb setup runs every phase; you can also run them individually
(emb deps, emb sync, emb flutter, emb engine).
Workspace & path resolution
Every command that touches the workspace resolves its root in this order:
--workspace <dir>(explicit flag), else- the
$FLUTTER_WORKSPACEenvironment variable, else - the current working directory.
<workspace>/ # the resolved root
app/ # cloned source repositories
flutter/ # Flutter SDK
bundle/ # built bundles (default output)
setup_env.sh # generated environment script
.config/flutter_workspace/
flutter-engine/<commit>/... # downloaded + extracted engine SDKs
flutter-engine/bundle-<mode>-<arch>/ # staged engine halves (icudtl + .so)
Command reference
emb <command> [arguments]
Global options
| Option | Description |
|---|---|
-h, --help |
Print usage. Works at the top level and per command (emb <cmd> --help). |
-v, --version |
Print the CLI version. |
--[no-]verbose |
Noisy logging, including every shell command executed. |
--workspace (-w), shown on most commands, follows the resolution order
above. --mode/--arch defaults and value sets differ per command — see
each entry.
emb doctor
Report host detection (os / arch / distro), package-manager backend
availability, and — when the backend can report it (PackageKit) — the count of
available package updates (from the backend's last cache refresh; doctor is
read-only and doesn't refresh). No options. Exit code is non-zero if the backend
is unavailable; the update check never fails the command.
emb doctor
emb setup
One-shot provision: deps → repos → Flutter SDK → engine, then writes
setup_env.sh. Each phase is individually skippable.
| Option | Default | Description |
|---|---|---|
-c, --config <dir> |
configs |
Legacy JSON config directory. |
-p, --packages <dir> |
— | Directory to discover self-describing emb manifests. |
--enable <id> |
— | Force-load the config with this id (overrides load: false). Repeatable; unmatched ids ignored. |
--disable <id> |
— | Skip the config with this id (overrides load: true). Repeatable; unmatched ids ignored. |
-w, --workspace <dir> |
resolution order | Workspace root. |
--flutter-version <ref> |
globals.json flutter_version |
Flutter version/tag/branch. |
--arch <arch> |
host arch | Engine arch to prefetch. |
-m, --mode <mode> |
release |
Engine runtime modes to prefetch (repeatable): release, profile, debug. |
-y, --yes |
off | Skip the deps confirmation prompt (CI). |
--skip-deps |
off | Skip host dependency install. |
--skip-sync |
off | Skip repository sync. |
--skip-flutter |
off | Skip Flutter SDK install. |
--skip-engine |
off | Skip engine artifact fetch. |
emb setup --config ../configs --yes
emb setup --config ../configs --skip-deps --skip-sync # SDK + engine only
emb setup --config ../configs --enable weston --disable agl-compositor
Config selection (applies to
setup,deps, andsync): legacy--configcomponents apply only when theirloadflag is logically true.--enable <id>/--disable <id>override that per component, matched byid; ids matching nothing are ignored, and load order is preserved.
emb deps
Coalesce host dependencies across all selected manifests, filter to what's missing on this host, and install the union in one transaction.
| Option | Default | Description |
|---|---|---|
-c, --config <dir> |
configs |
Legacy JSON config directory. Repeatable. |
-p, --packages <dir> |
— | Directory to discover self-describing emb manifests. Repeatable. |
--enable <id> |
— | Force-load the config with this id (overrides load: false). Repeatable; unmatched ids ignored. |
--disable <id> |
— | Skip the config with this id (overrides load: true). Repeatable; unmatched ids ignored. |
--dry-run |
off | Resolve and print the install plan without changing the system. |
-y, --yes |
off | Skip the confirmation prompt (CI). |
emb deps --config ../configs --dry-run # plan only
emb deps --config ../configs --yes # install in one transaction
emb sync
Clone/update source repositories into <workspace>/app (bounded concurrency).
| Option | Default | Description |
|---|---|---|
-c, --config <dir> |
configs |
Legacy JSON config directory. Repeatable. |
-p, --packages <dir> |
— | Directory to discover self-describing emb manifests. |
--enable <id> |
— | Force-load the config with this id (overrides load: false). Repeatable; unmatched ids ignored. |
--disable <id> |
— | Skip the config with this id (overrides load: true). Repeatable; unmatched ids ignored. |
--repos <file> |
— | A JSON file containing a bare array of repo entries. Repeatable. |
-w, --workspace <dir> |
resolution order | Workspace root. |
-j, --concurrency <n> |
4 |
Maximum concurrent git operations. |
emb sync --config ../configs -j 8
emb flutter
Install the Flutter SDK into <workspace>/flutter, then emit
<workspace>/setup_env.sh so FLUTTER_WORKSPACE (and the SDK's Flutter/Dart on
PATH) match the workspace just provisioned — i.e. the -w target.
| Option | Default | Description |
|---|---|---|
-w, --workspace <dir> |
resolution order | Workspace root. Also becomes FLUTTER_WORKSPACE in the emitted setup_env.sh. |
--flutter-version <ref> |
globals.json flutter_version |
Version/tag/branch to check out. |
-c, --config <dir> |
configs |
Directory to read globals.json from. |
--configure |
off | Run flutter config (desktop + custom devices) and flutter doctor after install. |
emb flutter -w /tmp/ws1 --flutter-version 3.44.2
. /tmp/ws1/setup_env.sh # FLUTTER_WORKSPACE=/tmp/ws1
emb engine
Fetch prebuilt Flutter engine artifacts (auto fetch-else-build). Modes you don't
fetch here are auto-fetched on demand by emb bundle/emb build.
| Option | Default | Description |
|---|---|---|
-w, --workspace <dir> |
resolution order | Workspace root. |
--commit <sha> |
<workspace>/flutter/bin/internal/engine.version |
Engine commit to fetch. |
--arch <arch> |
host arch | Engine arch token (see Architectures). |
-m, --mode <mode> |
release |
Runtime modes to fetch (repeatable): release, profile, debug. |
--clean |
off | Re-stage bundles even when already present. |
--check |
off | Only check prebuilt availability; do not download. |
emb engine --arch arm64 --mode release --mode profile
emb engine --arch riscv64 --check # is a prebuilt available?
emb aot
Build the AOT image (libapp.so) for a Flutter app — the AOT primitive used by
emb bundle --build. Debug is not AOT, so only release/profile apply.
| Option | Default | Description |
|---|---|---|
-a, --app-path <dir> |
mandatory | Path to the Flutter application to build. |
-w, --workspace <dir> |
resolution order | Workspace root. |
-m, --mode <mode> |
release |
Runtime modes to build (repeatable): release, profile. |
--arch <arch> |
host arch | Target arch for gen_snapshot (e.g. arm64 for a Pi). |
--gen-snapshot <path> |
auto-resolved | Explicit gen_snapshot path (overrides resolution). |
--glibc-sysroot <dir> |
artifact's bundled clang_x64/lib64 |
Directory with ld-linux + libc to run gen_snapshot under. |
emb aot --app-path ./app/my_app --arch arm64 --mode release --mode profile
emb bundle
Assemble an ivi-homescreen bundle from app + engine artifacts. With --build it
runs emb aot first; the engine SDK for the (mode, arch) is fetched
implicitly if not already cached.
| Option | Default | Description |
|---|---|---|
-a, --app-path <dir> |
mandatory | Path to the Flutter application. |
-w, --workspace <dir> |
resolution order | Workspace root. |
-m, --mode <mode> |
release |
Single mode: debug (JIT, no AOT), profile, or release (AOT). |
--arch <arch> |
host arch | Target arch (e.g. arm64). |
-o, --output <dir> (alias --out) |
<workspace>/bundle/<app>-<mode>-<arch> |
Output bundle directory — any path. |
--build |
off | Run emb aot first to (re)build flutter_assets + libapp.so. |
emb bundle --app-path ./app/my_app --arch arm64 --build # release
emb bundle --app-path ./app/my_app --arch arm64 --build --mode debug # JIT
emb bundle --app-path ./app/my_app --arch riscv64 --build --mode profile
emb bundle --app-path ./app/my_app --build # host
emb bundle --app-path ./app/my_app --arch arm64 --output /tmp/out # custom path
emb build
Build a self-describing package — a directory with an emb.yaml (or an
emb: key in pubspec.yaml) — into bundles, using the manifest's build:
matrix. The package directory is a positional argument (mandatory; omitting
it prints a usage error).
emb build <package-dir> [options]
| Option | Default | Description |
|---|---|---|
<package-dir> |
mandatory (positional) | Directory containing emb.yaml or a pubspec.yaml with an emb: key. |
-w, --workspace <dir> |
resolution order | Workspace root. |
--arch <arch> |
manifest's archs |
Override target arch(es). Repeatable. |
-m, --mode <mode> |
manifest's modes |
Override mode(s): debug, profile, release. Repeatable. |
--no-build |
off | Assemble from existing artifacts; skip compiling. |
emb build ./app/my_app # full manifest matrix
emb build ./app/my_app --arch arm64 --mode release # override the matrix
emb build ./app/my_app --no-build # assemble only
Manifest (emb.yaml)
id: my_app
type: app
build:
app_path: . # Flutter app dir, relative to this manifest (default ".")
archs: [arm64, x86_64] # target architectures (default: host)
modes: [release, debug] # default: [release]
output: bundles # optional output dir, relative to the workspace
deps: # optional host packages, by OS / distro
linux:
fedora: [pkg-config, freetype-devel]
ubuntu: [pkg-config, libfreetype-dev]
macos: [pkg-config, freetype]
windows: [Kitware.CMake]
Host dependencies (deps:)
The deps: block declares host build packages, keyed by OS, then optionally
by distro id on Linux:
- A list directly under an OS (
macos: [...],windows: [...], orlinux: [...]) applies to any distro on that OS. - A map under
linux(fedora: [...],ubuntu: [...], …) selects by the host's distro id (from/etc/os-release, whatemb doctorprints).
emb deps coalesces these across every selected manifest, filters to what's
missing on the current host, and installs the union in one transaction
(PackageKit on Linux, brew on macOS). Resolution is by package name, mapped to
the backend's real package via WhatProvides, so e.g. pkg-config resolves
even where the package is pkgconf.
A complete worked manifest (ivi-homescreen graphics deps for fedora/ubuntu/macOS)
is in example/ivi-homescreen/emb.yaml.
Inspect the resolved/missing plan for your host without installing:
emb deps --packages example --dry-run
Legacy
configs/*.jsondeclare deps differently — as inlinesudo … installstrings underruntime.pre-requisites[arch][distro][version]— from whichembextracts the package names. Both schemas normalize to the same per-host rule set.
emb cross
Cross-compile a native embedder (e.g. ivi-homescreen) for an arm64 /
riscv64 target from an x86_64 host, driven by the manifest's cross: block.
This is the C/C++ toolchain + sysroot path — distinct from the Dart AOT cross
used by emb build / emb bundle. Three providers: arm-gnu (a downloaded ARM
GNU toolchain plus a sysroot unpacked from a distro image or rsync'd from a
device), yocto-recipe (a located OE recipe-sysroot), and yocto-sdk (a
populate_sdk install). The input is a positional project dir (with a
.emb/ manifest directory or a top-level emb.yaml) or an explicit manifest
file.
emb cross <project-dir|manifest.yaml> [options]
| Option | Default | Description |
|---|---|---|
<project-dir|manifest> |
mandatory (positional) | Project dir (a .emb/ directory, else emb.yaml), or a manifest file. |
-w, --workspace <dir> |
resolution order | Workspace root. |
-t, --target <name> |
flat manifest's target, else local |
Select a target (e.g. rpi5, imx93-evk): a cross.targets entry or a per-board .emb/ file. local/host is a native build on this machine. With multiple targets, omitting --target defaults to local. |
--list-targets |
off | List the targets this project defines — cross.targets entries and .emb/ files, grouped by family — plus the built-in local, then exit. |
--dry-run |
off | Report the resolution plan (provider, toolchain, sysroot, preflight, augment, backends) with no download / mount / ssh. |
--prepare |
off | After resolving, build the augment libraries into the overlay. |
--build |
off | Configure + build the embedder under the resolved profile, one build per cross.backends entry. |
--backend <name> |
all | Build only the named cross.backends entries. Repeatable. |
--deb |
off | With --build: package each backend binary into a root-free .deb (Depends auto-derived from the binary's needed libraries). |
--app <dir> |
— | With --build: also build this Flutter app for the target and assemble a runnable bundle (embedder + engine + flutter_assets + icudtl + libapp), runnable as ./homescreen -b .. |
-m, --mode <mode> |
release |
Runtime mode for the --app bundle (debug/profile/release). |
--tar |
off | Also produce a .tar.gz of each runnable bundle. |
--deploy <user@host> |
— | With --app: send each runnable bundle to the board over SSH — rsync when the target has it, else a tar-over-SSH fallback (port/opts reused from cross.sysroot when device-sourced). |
--deploy-dir <path> |
ivi-homescreen |
Remote destination dir for --deploy. |
--run |
off | After --deploy, run the bundle on the target over SSH (./homescreen -b .). |
--clean |
off | Remove this target's build + overlay dirs (keeps the toolchain + sysroot), then exit. |
--clean-all |
off | Also remove the downloaded / extracted toolchain + sysroot and the apt / deb caches, then exit. |
--update-lock |
off | Regenerate this target's emb.lock entry from the resolved toolchain/sysroot (accepts an intentional URL / version change). See Reproducible builds. |
--no-verify |
off | Skip emb.lock verification for this resolve (don't fail on a drifted artifact sha or version). |
--host-tools |
off | With --build: use the host's cmake/meson instead of the SDK's, for OE SDKs that pin an old one (e.g. AGL ships cmake 3.16.5). The OE env + toolchain/cross file are unchanged. Also set via cross.host_build_tools. |
--install-deps |
off | Install the provider's missing preflight host tools via the host package backend (PackageKit/brew) instead of erroring. Opt-in; needs privileges. Falls back to printing the manual install command when no backend is reachable. |
--dockerfile |
off | Resolve, then emit a Dockerfile + .dockerignore (into the platform dir) that bake the toolchain + sysroot into an OCI image so CI pulls instead of resolving. arm-gnu only; does not build. See Toolchain images. |
emb cross ./app/ivi-homescreen --dry-run # plan only, no side effects
emb cross ./app/ivi-homescreen --build # toolchain + sysroot + build
emb cross ./app/ivi-homescreen --build --deb # ...and package a .deb
emb cross ./app/ivi-homescreen --clean # drop build dirs (keep toolchain)
emb cross ./app/ivi-homescreen --clean-all # drop everything for this target
Everything is root-free: the sysroot is extracted with debugfs /
dpkg-deb, and -dev packages are resolved against the image's own apt sources
— no apt, no chroot, no sudo. Validated end-to-end on Raspberry Pi
(arm-gnu, raspios bookworm): --build --deb produces an aarch64 ELF and an
installable .deb. See examples/cross/ for one manifest
per board (pi5 is the validated end-to-end example) and the full schema.
Manifest (cross: block)
cross:
provider: arm-gnu # arm-gnu | yocto-recipe | yocto-sdk
toolchain_version: 12.3.rel1 # pinned ARM GNU release (or version_policy: derive)
image_url: https://.../raspios-bookworm-arm64-lite.img.xz
cpu_flags: [-mcpu=cortex-a76] # pi5; pi4=cortex-a72, pi-zero-2=cortex-a53
sysroot:
partition: 2 # rootfs partition in the image (default 2)
dev_packages: [libdrm-dev, libegl-dev, libgbm-dev, libinput-dev]
augment: # libs built from source when the sysroot is too old
- { pkg: libdisplay-info, min: "0.2.0", url: https://.../libdisplay-info-0.2.0.tar.gz, build: meson, static: true }
defines: # -D<name>=<value> applied to every build
CMAKE_INSTALL_PREFIX: /usr
cmake_args: [-Wno-dev] # raw cmake configure flags (cmake only)
backends: # one build per entry; -D<key>=<value> each
drm-kms-egl: { BUILD_BACKEND_DRM_KMS_EGL: 'ON', DISABLE_PLUGINS: 'ON' }
package: # optional, consumed by --deb
name: ivi-homescreen
version: 1.0.0
bin: shell/homescreen # binary, relative to each backend build dir
install_dir: /usr/bin
Multiple platforms in one manifest (cross.targets)
To target several boards from a single manifest, put the shared config at the
cross: level and a per-board override under cross.targets, then pick one with
--target:
cross:
provider: arm-gnu # shared by every target
toolchain_version: 12.3.rel1
sysroot: { dev_packages: [libdrm-dev, libegl-dev, libgbm-dev, libinput-dev] }
backends: { drm-kms-egl: { BUILD_BACKEND_DRM_KMS_EGL: 'ON' } }
targets: # per-board overrides
rpi5: { image_url: …raspios…, cpu_flags: [-mcpu=cortex-a76] }
rpi4: { image_url: …raspios…, cpu_flags: [-mcpu=cortex-a72] }
rpi-zero-2w: { image_url: …raspios…, cpu_flags: [-mcpu=cortex-a53] }
radxa-zero3: { image_url: …radxa…, cpu_flags: [-mcpu=cortex-a55] }
emb cross . --list-targets
emb cross . --target rpi5 --build --deb
emb cross . --target radxa-zero3 --build
A target's fields shallow-merge over the shared block (a top-level image_url
folds into sysroot). Working dirs are content-hash-keyed, so boards that share
a sysroot (rpi4/rpi5/zero-2w — same image, only -mcpu differs) extract it
once, while a different image (radxa) gets its own. A manifest with no
cross.targets behaves exactly as before (one implicit target).
A .emb/ directory of per-board manifests
When the boards diverge enough that one file gets unwieldy (e.g. a Yocto board
with its own provider, recipe, and augment list next to a Raspberry Pi family),
put a .emb/ directory at the project root instead. emb cross <project>
prefers <project>/.emb/ over a top-level emb.yaml:
my-app/
.emb/
base.emb.yaml # shared: provider defaults, defines, package, sysroot deps
raspberry-pi.emb.yaml # a family file: cross.targets → rpi4, rpi5, rpi-zero-2w
imx93.emb.yaml # a flat per-board file: platform.name → imx93-evk
Each file declares a platform: block, and the selectable target list is the
union across every file:
# imx93.emb.yaml
platform:
name: imx93-evk # the --target value
description: NXP i.MX93 EVK (drm-kms-egl)
cross:
provider: yocto-recipe # overrides base.emb.yaml's provider
triple: aarch64-poky-linux
backends: { drm-kms-egl: { BUILD_BACKEND_DRM_KMS_EGL: 'ON' } }
emb cross my-app --list-targets # imx93-evk, rpi4, rpi5 [raspberry-pi], … + local
emb cross my-app --target imx93-evk --build
emb cross my-app --target rpi5 --build --deb
Resolution merges three layers: .emb/base.emb.yaml (a deep merge — nested
maps like cross.sysroot combine, so base defaults survive what a board omits),
then the board file's cross:, then a cross.targets[variant] shallow override.
A flat file contributes one target (named by platform.name); a family file
contributes one per cross.targets key, grouped under its platform.name.
Duplicate target names across files are an error. Native local uses
base.emb.yaml's shared block.
There's always a built-in local target (alias host): a native build on
this machine — no cross toolchain or sysroot, host compiler + system libraries
(install host dev deps via emb deps). It's the default when a manifest
defines cross.targets and you don't pass --target, so emb cross . --build
builds for the dev box while --target rpi5 cross-builds.
emb cross . --build # native local build (default with cross.targets)
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:
# 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 |
|---|---|
<board> |
a target in emb's board library (the hardware layer) |
<dir>#<target> |
a target in another emb project at <dir> (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:
# 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). <dir> 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,
keyed by target — so a moved or changed URL fails loudly instead of silently
building against different bytes. It is written for real resolves only;
--dry-run never touches it.
On the first resolve of a target, emb.lock is created automatically
(pub-style). On later resolves the toolchain/sysroot is re-verified against
it and the build fails on drift; --update-lock accepts the change (and
rewrites the entry), --no-verify skips the check for one run. Commit emb.lock
so CI and teammates resolve the same inputs.
What each provider pins:
| Provider | Pinned facts |
|---|---|
arm-gnu |
sha256 of the toolchain tarball and the distro image (byte-exact); a device-sourced sysroot records provenance only — a live host can't be content-pinned. |
yocto-sdk |
OECORE_SDK_VERSION, plus the sha256 of the populate_sdk installer when fetched from sdk_url. |
yocto-recipe |
the located recipe version + the native gcc version (it downloads nothing, so there is no artifact to sha). |
Drift is reported for: a changed artifact sha (moved URL), a changed resolved /
derived version, or edited manifest inputs (the content-addressed
sysroot_key / build_key). An artifact not re-fetched on a warm cache is
skipped, so verification fires exactly when bytes are re-materialized.
emb cross . --target rpi5 --build # first run → writes emb.lock
emb cross . --target rpi5 --build # later → verifies, fails on drift
emb cross . --target rpi5 --build --update-lock # accept an intentional change
Note:
emb.locklives at the project root keyed by target name, so loosely co-located single-file manifests that share one directory would collide on thedefaulttarget. One project = one directory is the intended layout.
Toolchain images
Downloading + extracting an arm-gnu toolchain and sysroot is the slow part of a
cold build. --dockerfile resolves the target, then emits a Dockerfile +
.dockerignore into the platform dir that bake the toolchain + sysroot into an
OCI image (with the host build tools — cmake, ninja, wayland-scanner, …) — so CI
pulls a ready toolchain instead of re-resolving:
emb cross . --target rpi5 --dockerfile
# Wrote …/cross-aarch64-none-linux-gnu-<key>/Dockerfile
# Build: docker build -t emb-cross-aarch64-none-linux-gnu:<key> …/cross-…-<key>
docker build -t emb-cross-aarch64-none-linux-gnu:<key> <that dir>
The image bakes them at a fixed workspace (/emb) under the same
cross-<triple>-<key> path emb computes, so consuming it needs no special
flag — a build in the image resolves as a cache hit:
# CI: run the cross build against the prebuilt image
container: emb-cross-aarch64-none-linux-gnu:<key>
# → emb cross <manifest> --target rpi5 --build -w /emb (skips download/extract)
To run the same build locally (no CI), mount your project, pub cache, and Dart SDK into the image — only the toolchain + sysroot are baked, emb itself is not:
docker run --rm \
--security-opt label=disable \ # Fedora/SELinux only; omit elsewhere
-v "$PROJECT:$PROJECT" -v "$PUB_CACHE:$PUB_CACHE" -v "$DART_SDK:$DART_SDK:ro" \
-e "PATH=$DART_SDK/bin:$PATH" -e "PUB_CACHE=$PUB_CACHE" -w "$PROJECT" \
emb-cross-aarch64-none-linux-gnu:<key> \
emb cross "$PROJECT" --target rpi5 --build --backend wayland-egl -w /emb
The <key> (the sysroot_key) must match the manifest; change the manifest and
you rebuild the image. Always (re)build the image from the current emitter —
it bakes the host build tools the embedder needs (cmake, ninja, meson,
wayland-scanner, git/curl); an older image is missing them. augment
libraries aren't baked — --build rebuilds them into the cached sysroot at
consume time (cheap). arm-gnu only for now.
The baked sysroot is slimmed to a cross-build sysroot via .dockerignore: the
device rootfs's runtime data, bundled apps, docs, kernel/firmware, and target
executables are dropped, keeping headers, libraries, pkgconfig/cmake metadata,
and the wayland protocol XMLs. (For the radxa zero3 image this took the sysroot
from 7.4 GB to 4.7 GB — image ~6 GB — with all three backends still building.)
emb env
(Re)generate or print setup_env.sh (PATH for Flutter/Dart,
FLUTTER_WORKSPACE, PUB_CACHE, XDG_CONFIG_HOME, the engine version, …).
emb setup and emb flutter already emit this file, so the standalone command
is for what they don't cover: regenerating after the workspace has moved
(the baked-in FLUTTER_WORKSPACE is absolute), previewing the env on stdout, or
writing it to a custom path. It has no side effects beyond the one file — it
never clones or installs, and warns if <workspace>/flutter is absent (the env
would point at a missing SDK; run emb flutter -w <root> first).
| Option | Default | Description |
|---|---|---|
-w, --workspace <dir> |
resolution order | Workspace root (becomes FLUTTER_WORKSPACE). |
-o, --output <path> |
<workspace>/setup_env.sh |
Output file path. |
--print |
off | Print to stdout instead of writing a file. |
emb env # (re)write <workspace>/setup_env.sh
emb env -w /tmp/ws1 # regenerate after moving the workspace
emb env --print # preview on stdout
emb update
Update the CLI itself (via pub). No options.
emb update
Modes
--mode |
How it builds | Bundle contents | Valid in |
|---|---|---|---|
debug |
JIT — flutter build bundle --debug |
kernel_blob.bin, no libapp.so |
bundle, build, engine, setup |
profile |
AOT (gen_snapshot) |
libapp.so (+ profile engine) |
all |
release |
AOT (gen_snapshot) |
libapp.so (+ release engine) |
all |
emb aot only accepts release/profile (debug isn't AOT). Where a command
takes multiple modes (aot, build, engine, setup), repeat the flag:
--mode release --mode profile. emb bundle takes a single mode.
A bundle is the directory ivi-homescreen consumes:
<bundle>/
data/flutter_assets/ # app code + assets
data/icudtl.dat # from the engine
lib/libflutter_engine.so # from the engine (per mode)
lib/libapp.so # AOT image (profile/release only)
Architectures & cross-compiling
--arch accepts machine names or Flutter tokens; they normalize to one of four
engine arch tokens used in paths and bundle names:
| You pass | Normalized token |
|---|---|
x64, x86_64, amd64 |
x86_64 |
arm64, aarch64 |
arm64 |
arm, armv7, armv7hf, armhf |
armv7hf |
riscv64 |
riscv64 |
Cross-compile is x86_64 host → target arch. The meta-flutter engine SDK
ships a host-x86_64 simulator gen_snapshot that emits target code and
carries its own loader/libc in clang_x64/lib64, so it runs on any host glibc —
no qemu required. emb selects and runs it automatically; --gen-snapshot
/ --glibc-sysroot override if needed. Building on an arm64 host targets
arm64 natively.
Development
dart pub get
dart analyze
dart test
The macOS/Windows package backends live in lib/src/pkg/_platform/ and are
excluded from the default (Linux) build — see that folder's README.md. Dart
has no OS-conditional dependencies, so they're wired in only on per-OS builds
that add brew_dart / winget_dart.