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, Homebrew
on macOS, WinGet on Windows), 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.
- Dart SDK ≥ 3.10.1 to run/build
emb— or none preinstalled:bootstrap.sh(Linux/macOS) orbootstrap.ps1(Windows) fetches a pinned SDK (see Install). - Python 3.6+ for the bootstrap script.
- Linux: PackageKit for host dependency install (dnf/apt/zypper).
- macOS: Homebrew for host dependency install.
- Windows: WinGet for host dependency install (Windows 10 1809+).
gitonPATH.- Cross-compiling to a device arch is supported from an x86_64 host.
Quickest path — no preinstalled Dart or Flutter needed. The bootstrap script
fetches a pinned Dart SDK and activates emb; --shellenv prints the
shell-specific PATH update for the current session.
git clone https://github.com/toyota-connected/emb_cli.git && cd emb_cli
eval "$(./bootstrap.sh --shellenv)"
emb --versiongit clone https://github.com/toyota-connected/emb_cli.git; cd emb_cli
Invoke-Expression (.\bootstrap.ps1 --shellenv)
emb --versionA bootstrap.cmd wrapper is also provided for CMD users (does not support
--shellenv; manually add the printed bin directories to PATH).
# From the package root:
dart pub get
dart pub global activate --source=path . # puts `emb` on PATH
emb --helpMake 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 rootThe 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.
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-eglEach 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/.
# 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-arm64emb setup runs every phase; you can also run them individually
(emb deps, emb sync, emb flutter, emb engine).
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)
emb <command> [arguments]
| Option | Description |
|---|---|
-h, --help |
Print usage. Works at the top level and per command (emb <cmd> --help). |
--version |
Print the CLI version. |
-v, --verbose |
Stream toolchain output live, prefixed per step ([cmake:…], [ninja:…]). |
-vv |
Everything -v does, plus diagnostic logging (every shell command, resolved env). |
-q, --quiet |
Errors only; suppress progress and info logging. |
Verbosity may also be set with EMB_VERBOSITY=0\|1\|2 (used when no flag is
passed). Note --version is long-only — -v now means verbose.
--workspace (-w), shown on most commands, follows the resolution order
above. --mode/--arch defaults and value sets differ per command — see
each entry.
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). Exit code is non-zero if the backend is
unavailable; the update check never fails the command. --json emits a
machine-readable {schema, command, ok, data} envelope instead of text.
With --target <name> it instead reports a cross target's provider
preflight — the host tools that target's provider needs (e.g. tar/xz/
rsync for arm-gnu), each present or missing, with an install hint — resolving
the manifest at the positional path (default: the current directory). Exit code
is non-zero when a required tool is missing.
With --offline-probe --target <name> it certifies the target's
offline build closure is materialized: it resolves the
toolchain + sysroot from the store with the network denied, confirms each cargo
module is vendored, and (under --strict) that network isolation is available.
No build runs — it checks inputs only, so it stays a seconds-scale gate for CI
to run after emb fetch. Exit code is non-zero (with the fix, emb fetch) when
anything is missing.
emb doctor
emb doctor --json | jq .data.backend
emb doctor --target rpi5-bookworm examples/cross/pi5.emb.yaml
emb doctor --target rpi5-bookworm . --json | jq .data.target.preflight
emb doctor --offline-probe --target rpi5 . # is the closure ready for an offline build?
emb doctor --offline-probe --strict --target rpi5 . --json | jq .data.offline_probeOne-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-compositorConfig 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.
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 transactionClone/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 8Install 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/ws1Fetch 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?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 profileAssemble 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 pathBuild 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 onlyid: 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]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-runLegacy
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.
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. |
--json |
off | Emit the plan as a machine-readable {schema, command, ok, data} envelope instead of text (implies --dry-run). Also on emb doctor. |
--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). |
--fetch-only |
off | Resolve and materialize the toolchain + sysroot closure (and pin emb.lock), then stop before configuring or building — the online acquisition step. Same work as emb fetch. See Offline builds. |
--offline |
off | Deny all network access: reuse already-cached toolchain/sysroot inputs and fail on a miss (run --fetch-only online first). Cargo modules build against their vendored crates (CARGO_HOME/CARGO_NET_OFFLINE). See Offline builds. |
--offline-strict |
off | Like --offline, but also run build subprocesses inside a network namespace, and refuse to build when that isolation is unavailable instead of degrading to input-level denial. See Offline builds. |
--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 targetEverything 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.
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]
snapshot: 2024-06-01 # pin apt resolution to a mirror snapshot (optional)
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/binTo 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 --buildA 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).
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 --debResolution 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 --buildemb 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'sextends 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).
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); the resolved -dev package versions + digests; 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). |
The lock also records a root env: block — the host tool versions the resolve
ran with (emb, the engine and Flutter commits, rustc) — so a rebuild years
later can tell whether a difference came from a changed tool rather than a
changed input.
Drift is reported for: a changed artifact sha (moved URL), a changed -dev
package version, 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.
Apt snapshots. By default dev_packages resolve against the live mirror, so
the same names can pull different versions over time. Set sysroot.snapshot: to
a date (2024-06-01, or a full YYYYMMDDTHHMMSSZ stamp) to pin resolution to a
snapshot.debian.org / snapshot.raspbian.org
mirror, so a build resolves the same -dev versions for the life of the
product. The date folds into the sysroot store key, so changing it re-extracts;
a source with no known snapshot service warns and falls back to the live mirror.
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 changeNote:
emb.locklives at the project root. A.emb/project keys its entries by target name (unique across the project). A flat single-file manifest keys its entry by<manifest-stem>:<target>, so several loosely co-located*.emb.yamlsharing one directory get independent entries in the sharedemb.lockinstead of clobbering one another.
Acquisition and building are separate: fetch every input once while online, then build with the network denied. This is what a long-support-window product needs — a build that never asks the network for anything it didn't already archive.
emb fetch <project> [--target <t>] [--app <dir>](oremb cross … --fetch-only) resolves and materializes the toolchain + sysroot closure — including the apt-devset — into the shared store and pinsemb.lock. It also vendors eachbuild: cargomodule's crates (cargo vendor --locked, keyed by itsCargo.lock) into the store, and with--apprunsflutter pub get --enforce-lockfileinto a store-rootedPUB_CACHE. Then it stops. Everything now lives under the shared cache, so oneemb cache exportescrows the whole closure.emb cross … --build --offlinethen builds with all network access denied: the content store serves a cached blob or fails closed rather than downloading, the apt path refuses to reach out, cargo modules build against the vendored crates (CARGO_HOME→ vendor dir,CARGO_NET_OFFLINE,--offline), and the app'sflutter build bundlereads from the same store-rootedPUB_CACHE— so nothing touches a registry. A miss names the missing artifact and points you back at the fetch step.
--offline denies emb's own egress and fast-fails cargo, but trusts a build
subprocess to honor it. --offline-strict additionally runs build subprocesses
inside a rootless network namespace (unshare --net, loopback only), so a build
script that reaches for the network is denied outright — and it refuses to
build when that isolation is unavailable (common on locked-down CI runners and
inside containers where unprivileged user namespaces are disabled) rather than
silently falling back to the weaker guarantee.
emb fetch . --target rpi5 --app ./app/my_app # online: toolchain, sysroot, crates, pub
emb cross . --target rpi5 --build --offline # offline: build, no network
emb cross . --target rpi5 --build --offline-strict # ...and sandbox build subprocessesPin dev_packages with sysroot.snapshot: (above) so the offline build resolves
the same package versions every time. In CI, gate on
emb doctor --offline-probe after the fetch step to prove the
closure is complete before the network is cut. To keep the closure for the long
haul, emb cache export writes it to a single self-verifying
.tar.zst that emb cache import restores on another machine or years later.
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 /embThe <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.)
Materialize a cross target's toolchain + sysroot closure into the shared store
and pin emb.lock, then stop — the online acquisition step for an
offline build. It resolves the toolchain + sysroot (no
configure/build), vendors each build: cargo module's crates, and with
--app prefetches the app's pub packages — so emb cross … --build --offline
afterwards needs no network. Native (local/host) targets skip the toolchain
resolve but still vendor crates and prefetch pub.
emb fetch <project-dir|manifest.yaml> [options]| Option | Default | Description |
|---|---|---|
<project-dir|manifest> |
mandatory (positional) | Project dir or manifest file (same resolution as emb cross). |
-t, --target <name> |
manifest default | Target to fetch; a cross.targets entry or a per-board .emb/ file. |
-w, --workspace <dir> |
resolution order | Workspace root. |
--app <dir> |
— | Also prefetch this Flutter app's pub packages (flutter pub get --enforce-lockfile) into the store-rooted PUB_CACHE, so they are part of the offline closure the escrow archives. |
--update-lock |
off | Regenerate this target's emb.lock entry from the resolved toolchain/sysroot. |
--no-verify |
off | Skip emb.lock verification for this resolve. |
emb fetch . --target rpi5 --app ./app/my_app # toolchain + sysroot + crates + pub
emb cross . --target rpi5 --build --offline # then build with no network(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 stdoutUpdate the CLI itself (via pub). No options.
emb updateInspect and reclaim the shared artifact cache — a content-addressed store of
toolchain and engine artifacts, downloaded once per machine and shared across
every workspace and target. Location resolves as $EMB_CACHE_DIR, else
$XDG_CACHE_HOME/emb, else ~/.cache/emb.
| Subcommand | Description |
|---|---|
path |
Print the resolved cache directory. |
list [--json] |
List store entries (kind, key, size, live references). |
gc [--dry-run] |
Remove incomplete entries and unreferenced, stale ones; report space reclaimed. |
migrate [-w <ws>] [--dry-run] |
Adopt a workspace's existing toolchain/engine trees into the store (moved in place of a re-download), leaving symlinks behind. |
push [<kind>/<key> …] --registry <r> [--repo <r>] [--force] [--dry-run] [--json] |
Upload store entries to an OCI registry as artifacts (via oras), so a team/CI shares prebuilt trees. Defaults to every complete entry; skips refs that already exist unless --force. |
pull <kind>/<key> … --registry <r> [--repo <r>] [--link <dir>] [--json] |
Download named entries from the registry into the store (extract-staged like a local build); --link also symlinks each into <dir>. |
export <archive.tar.zst> [--cas-only] [--image <ref>] |
Package the offline build closure (content-addressed blobs, extracted trees, apt -dev set, vendored crates, pub packages) into one deterministic .tar.zst escrow archive. --cas-only keeps just the blobs (~half the size; trees re-extract on an offline resolve). --image pins the build-environment image (from emb cross --dockerfile) in the manifest — use a digest ref. See Offline builds. |
import <archive.tar.zst> [--json] |
Restore an escrow archive into the cache, merging with what's there (content-addressed entries are identical, so overlap is safe); reports the pinned build-environment image, if any. |
The OCI registry (push/pull) is the live mirror under your control; an
escrow archive (export/import) is the offline copy — a single file you own
that reconstructs the closure years later, self-verifying on restore because its
blobs are content-addressed. The archive is data, though — years on it still
needs a runnable environment (emb, the Flutter SDK, a period-appropriate host).
Pin that with --image <ref>@sha256:…, a container image emb can emit via
emb cross --dockerfile, so the escrow references the
environment that reconstructs the closure, not just the bytes.
push/pull speak to an OCI registry through oras (one static binary on
Linux/macOS/Windows, no daemon). Set the registry with --registry or
$EMB_CACHE_REGISTRY and authenticate beforehand with oras login (it reuses
~/.docker/config.json). The ref for an entry is
<registry>/<repo>:<kind>-<key> (repo defaults to emb-cache). Entry keys are
identity-based, so a puller must name the exact <kind>/<key> (from emb cache list on the producer, or a prior resolve).
emb cache path
emb cache list --json | jq '.data.entries'
emb cache gc --dry-run
emb cache migrate --dry-run # preview adoptions for the current workspace
oras login ghcr.io # once, to authenticate
emb cache push --registry ghcr.io/acme --dry-run # preview refs
emb cache pull toolchain/arm-gnu-12.3.rel1-x86_64-aarch64-none-linux-gnu \
--registry ghcr.io/acme
emb cache export product-lts-2024.tar.zst # escrow the whole offline closure
emb cache import product-lts-2024.tar.zst # ...restore it on another machine--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)
--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.
dart pub get
dart analyze
dart testAll three host-package backends (PackageKit, Homebrew, WinGet) are compiled into every build. Each package's native build hook no-ops on unsupported platforms.