- What are Layers?
- How are Layers Used?
- X-Env Metadata
- Configuration Variables
- Device and Image Layers
- Specifying Layers
- How to Use This Documentation
- Getting Started
What are Layers?
Layers are modular, composable components that define specific aspects of your Raspberry Pi build. Each layer encapsulates a specific and focussed piece of functionality and can declare dependencies on other layers, creating a flexible build system. Layers have been loosely grouped into categories with a super-set being as follows.
-
Device Layers: Hardware-specific configuration for a particular device type. For example, may only install firmware applicable to Pi5.
-
Image Layers: Image-specific configuration to produce an image with a defined footprint / on-disk layout. For example, an image capable of supporting AB booting for OS and system slot redundancy.
-
General Layers: Reusable configuration snippets, utilities, groups of related functionality, packages and/or scripts. For example, local system user account creation, basic networking, etc.
-
Suites: A layer providing a particular baseline of device functionality. Typically, these type of 'profile' layers are agnostic to the underlying device. Using a suite allows a single layer to recursively pull in other layers to provide a common base of functionality such as a desktop or SDK, or a minimal set of userland components providing apt and networking, container utilities, etc.
How are Layers Used?
rpi-image-gen uses custom metadata in a layer to declare variables and construct a deterministic layer build order then passes this to bdebstrap (https://github.com/bdrung/bdebstrap) which aggregates and merges everything together into a configuration. That configuration is handed off to mmdebstrap (https://gitlab.mister-muffin.de/josch/mmdebstrap) which is the engine that drives creation of the filesystem. By using bdebstrap, layers provide a structured way to configure and extend mmdebstrap’s capabilities:
Layer Structure
Each layer is a YAML file containing:
-
X-Env Metadata: Mandatory. Layer attributes, variable declarations, dependencies
-
mmdebstrap configuration: Optional. Package lists, repository mirrors, scripts
Build Process
-
Parameter Assembly: Config file, cmdline and CLI establish desired layers, environment variables and search paths
-
Layer Resolution: Layer dependencies determine build order
-
Variable Expansion: Environment variables are validated and expanded using defined rules
-
Configuration Merging: Layer are merged
-
mmdebstrap Execution: The merged configuration drives mmdebstrap to create the filesystem
-
Processing: Additional layer-specific scripts are executed at defined points, eg setup, essential, customize, cleanup, etc
-
Post-Processing: filesystem overlays, SBOM generation, image creation, hooks
Key Benefits of Layers
-
Modularity: Mix and match layers to create custom images
-
Reusability: Share common configurations across different builds
-
Validation: Built-in variable validation prevents configuration errors
-
Dependencies: Automatic resolution of layer prerequisites
-
Customisation: Override defaults through environment variables set via the config file
X-Env Metadata
Layers use a custom X-Env metadata schema that strictly follows the DEB822 format (https://repolib.readthedocs.io/en/latest/deb822-format.html) embedded within YAML comment blocks. The metadata is delimited by # METABEGIN and # METAEND markers and parsed using the standard python3-debian DEB822 parser. By using metadata fields a layer can define attributes, dependencies, and configuration variables. The metadata is parsed separately from the standard YAML content.
Metadata Structure
X-Env metadata is contained within comment blocks:
# METABEGIN
# X-Env-Layer-Name: This layer's name
# X-Env-Layer-Description: Brief description of what this layer does
# X-Env-Layer-Category: device
# X-Env-Layer-Requires: base-layer,required-layer
# X-Env-VarPrefix: device
# X-Env-Var-hostname: pi-${HOSTNAME_SUFFIX}
# X-Env-Var-hostname-Description: System hostname for the device
# X-Env-Var-hostname-Valid: regex:^[a-zA-Z0-9.-]+$
# X-Env-Var-hostname-Set: immediate
# METAEND
# mmdebstrap YAML configuration follows...
mmdebstrap:
packages:
- systemd
- network-manager
Static vs Dynamic Layers
Layers are static unless explicitly marked dynamic. To render a layer at build time add:
# X-Env-Layer-Type: dynamic
# X-Env-Layer-Generator: myscript
The loader then:
-
Checks for the existence of a temporary directory specified by tag
DYNlayer=<tmpdir>on the layer search path (rpi-image-gendoes this automatically). -
Runs the generator with two arguments,
<template-path> <output-path>, where the output is written toDYNlayer/path/to/<layer-name>.yaml. Thepath/toportion matches the layer’s path under its original search root, so dynamic layers mirror the same directory structure as static layers when reported via tagged paths. -
Reads all mmdebstrap/env content from the generated file while continuing to use the original
# X-Env-*metadata for naming, dependencies, documentation, etc.
Generators must write a complete layer to the output path. A trivial generator could copy the template directly to the output (e.g. cp works fine). More more complex generators may inject snapshot timestamps, merge user data, or pull content from other sources. If the generator writes nothing, the layer still 'exists' (metadata is intact) but it contains no YAML body information (packages, hooks, etc) and --describe output will reflect that. Dynamic layers provide a way to manipulate the layer YAML at build time. Modifications made by the generator only affect the YAML body. The original # X-Env-* metadata is always taken from the template.
|
Important
|
|
The generator field supports optional arguments after the executable name. These are passed to the generator after <template-path> and <output-path>, so the full invocation is generator <template-path> <output-path> [args…]. Environment variables in arguments are expanded before the generator is called. Both positional arguments and flags are supported.
# X-Env-Layer-Generator: myscript ${IGTOP}/path/to/some/file
# X-Env-Layer-Generator: myscript -f ${IGTOP}/path/to/some/file -x
Generators that use argparse or getopt should parse from argv[3:], as argv[1] and argv[2] are always the fixed input and output paths respectively.
|
Note
|
|
Layer Attributes
X-Env-Layer-Name: Layer name
X-Env-Layer-Description: Human-readable description of the layer’s purpose
X-Env-Layer-Category: Categorisation (device, image, etc.)
X-Env-Layer-Version: Version identifier for the layer
X-Env-Layer-Requires: Comma-separated list of required layers
X-Env-Layer-Provides: Services or capabilities this layer provides
X-Env-Layer-RequiresProvider: Services or capabilities this layer requires
X-Env-Layer-Conflicts: Layers that cannot be used together with this one
X-Env-Layer-Type: Static (default) or dynamic
X-Env-Layer-Generator: If dynamic, the generator invoked by the loader
X-Env-Layer-Sets: Space-separated KEY=VALUE pairs injected into the build environment when this layer is present
Dependencies and Providers
X-Env-Layer-Requires
-
Direct layer references: "I need these specific layers"
-
Concrete dependencies: Must reference actual layer names
-
Environment variable expansion: Supports
${VAR}syntax for dynamic dependencies -
Build order enforcement: Dependencies are loaded first and are pull in automatically
-
Example: A device layer depends on a device base-layer because the base-layer provides mandatory settings inherited by the device layer.
# METABEGIN
# X-Env-Layer-Name: conditional-layer
# X-Env-Layer-Requires: base-layer,${ARCH}-toolchain,${DISTRO}-packages
# METAEND
Environment variables in dependencies are evaluated during layer discovery using the current environment context. If a required environment variable is not set, layer discovery will fail.
This enables dynamic layer dependency resolution based on build-time variables such as:
-
Architecture:
${ARCH}-toolchainresolves toarm64-toolchainwhenARCH=arm64 -
Distribution:
${DISTRO}-packagesresolves todebian-packageswhenDISTRO=debian -
Build variant:
${VARIANT}-configfor different build configurations
|
Important
|
Only variables present in the environment can be used in dependencies. |
X-Env-Layer-Provides / X-Env-Layer-RequiresProvider
-
Abstract capability requirements: "I need something that provides X"
-
Service/capability contracts: Multiple layers could satisfy the requirement
-
Flexible implementation: Any layer providing the capability can fulfill it
-
Relationships: If a provider is required, only one can exist in the overall configuration
-
Example: A layer requires a device provider, which could be satisfied by different device layers
|
Important
|
Unlike dependencies, environment variables are not supported when evaluating providers. |
X-Env-Layer-Sets
Used to inject KEY=VALUE pairs into the build environment when this layer is present. Later layers override earlier ones.
|
Important
|
Typically used for internal build/feature flags that do not belong in the |
Environment Variables
X-Env-VarPrefix: Prefix for all variables declared by this layer (e.g., device)
X-Env-VarRequires: Comma-separated list of variables this layer expects from other layers
X-Env-Var-<name>: Variable declaration with default value (supports placeholders like ${DIRECTORY})
X-Env-Var-<name>-Description: Human-readable description of the variable
X-Env-Var-<name>-Valid: Validation rule (type, range, regex, enum, etc.)
X-Env-Var-<name>-Set: Set policy (immediate, lazy, force, skip)
X-Env-Var-<name>-Triggers: Derived actions based on the variable value
X-Env-Var-<name>-Conflicts: Variables that cannot be set when this variable is also set
Variable Naming Convention
Variables follow the pattern: IGconf_<prefix>_<name>
-
Layer declares:
X-Env-Var-hostnamewith prefixdevice -
Environment variable:
IGconf_device_hostname -
Template expansion: Can reference as
${IGconf_device_hostname}in YAML values
Placeholder Support
Variable values support dynamic placeholders:
${DIRECTORY}: Directory containing the layer file
${FILENAME}: Name of the layer file (without extension)
${FILEPATH}: Full path to the layer file
Triggers
Triggers (X-Env-Var-*-Triggers) allow a variable’s resolved value to automatically perform actions, meaning that derived settings can be injected based on that value. Format as follows (one rule per line):
-
Conditional (same variable):
when=VALUE set TARGET=VALUE [policy=force|immediate|lazy] -
Conditional (same variable, wildcard):
when=* set TARGET=VALUE [policy=force|immediate|lazy] -
Conditional (cross-variable):
when=VAR=VALUE set TARGET=VALUE [policy=force|immediate|lazy] -
Conditional (cross-variable):
when=VAR!=VALUE set TARGET=VALUE [policy=force|immediate|lazy] -
Conditional (cross-variable, wildcard):
when=VAR=* set TARGET=VALUE [policy=force|immediate|lazy] -
Unconditional:
set TARGET=VALUE [policy=force|immediate|lazy]
For same-variable conditions, matching is an exact string comparison against the source variable’s resolved value. For cross-variable conditions, matching is an exact string comparison against the referenced variable’s resolved value (ie, the referenced variable is used as-is). Unconditional rules always fire. The first token after the action must be TARGET=VALUE. policy= is optional and defaults to immediate if not specified. If a cross-variable condition references an unknown variable, resolution fails with an error.
The wildcard * matches any non-empty value. It will not fire if the variable is unset or set to an empty string.
Lint will fail on:
-
Unknown actions.
-
Actions that do not start with
TARGET=VALUE. -
Target names that are not POSIX identifiers.
Values containing spaces must be quoted (single or double quotes):
-
when=erofs set IGconf_fs_erofs_mkfs_args="-b 4096 -z zstd,3" policy=lazy
Quoted values have the quotes stripped during parsing, with the stored value being the unquoted content. Variable expansion (${VAR}) within trigger values is deferred and resolved during the expansion phase, not at parse time.
Usage:
-
Target values are preserved as literals with no expansion at parse time (deferred expansion is supported via
${VAR}syntax). -
Triggers are collected from every definition of a variable across all layers. Because trigger rules are merged across layers and evaluated against the effective final (resolved) value, upstream layer trigger rules can still fire when a downstream layer declaration determines that final value. Suppression of upstream triggers is not supported.
Supported actions
| Action | Description | Default policy | Applies to |
|---|---|---|---|
|
Inject an environment variable with the given value and policy. |
immediate |
X-Env-*-Var |
Examples
-
Conditional:
# X-Env-Var-rootfs_type-Triggers: when=btrfs set IG_HAS_BTRFS_PROGS=1 policy=force -
Unconditional:
# X-Env-Var-rootfs_type-Triggers: set IG_ALWAYS_ON=1 policy=immediate -
Multiple with different conditions (new line per condition helps readability, but is not mandatory):
# X-Env-Var-rootfs_type-Triggers: # when=btrfs set IG_HAS_BTRFS_PROGS=1 policy=force # when=ext4 set IG_HAS_E2FS_PROGS=1 policy=force -
Quoted value with deferred variable expansion:
# X-Env-Var-rootfs_type-Triggers: when=erofs set IGconf_fs_erofs_mkfs_args="-b ${IGconf_linux_page_size} -z zstd,3" policy=lazy
|
Important
|
Trigger precedence is determined by layer order, not file order.
Within a single layer:
|
Example (same layer)
# X-Env-VarPrefix: image
#
# X-Env-Var-deploy_type: develop
# X-Env-Var-deploy_type-Valid: develop,production
# X-Env-Var-deploy_type-Set: y
# X-Env-Var-deploy_type-Triggers: when=production set IGconf_image_pmap=crypt policy=force
#
# X-Env-Var-pmap: clear
# X-Env-Var-pmap-Valid: clear,crypt
# X-Env-Var-pmap-Set: y
# X-Env-Var-pmap-Set: force
If IGconf_image_deploy_type resolves to production, the trigger injects IGconf_image_pmap=crypt after the source variable, overriding pmap: clear in the same layer. A later layer defining IGconf_image_pmap with force would still win over the trigger.
Example (cross-var across layers)
# X-Env-VarPrefix: device
# X-Env-Var-storage_type: sd
# X-Env-Var-storage_type-Valid: sd,emmc
# X-Env-Var-storage_type-Set: y
# X-Env-VarPrefix: image
# X-Env-Var-ptable_protect: n
# X-Env-Var-ptable_protect-Triggers: when=IGconf_device_storage_type=emmc set IGconf_image_ptable_protect=y
If IGconf_device_storage_type resolves to emmc, the trigger injects IGconf_image_ptable_protect=y.
Variable Conflicts
Variable conflicts (X-Env-Var-*-Conflicts) allow a variable to declare that it cannot be set when other variables are also set, or when other variables do/do not have a particular value. Conflict resolution takes place on the final set of resolved IGconf prefixed variables. When declaring conflicts, any variable name can be specified (fully prefixed) that exists in the final set - not just variables declared in the same layer, ie cross-layer conflict resolution is supported. If a variable is set in a base layer and overridden in a consumer layer, conflicts are defined by the winning definition, ie conflicts from the earlier definition are not inherited.
Conflict expressions adhere to <precursor> <condition> syntax. The precursor is optional (when=<value>) and a single expression may only include one precursor. The condition can be unconditional (name) or conditional (name=value / name!=value). Multiple expressions can be specified, separated by commas, and may be wrapped across multiple lines. Each expression may include only one precursor.
Examples:
# Unconditional:
# X-Env-Var-user1pass:
# X-Env-Var-user1pass-Conflicts: user1hashpass
#
# X-Env-Var-user1hashpass:
# X-Env-Var-user1hashpass-Conflicts: user1pass
# Conditional (no precursor):
# X-Env-Var-storage_type: sd
# X-Env-Var-storage_type-Valid: sd,emmc,nvme
#
# X-Env-Var-lockemmc: y
# X-Env-Var-lockemmc-Valid: bool
# X-Env-Var-lockemmc-Conflicts: storage_type!=emmc
# Conditional (with precursor):
# X-Env-Var-lockemmc-Conflicts: when=y storage_type!=emmc
Configuration Variables
The environment variables declared by a layer customise build behavior:
-
Validation: Each variable includes validation rules (types, ranges, patterns)
-
Placeholders: Support for dynamic values like
${DIRECTORY}and${FILENAME} -
Set Policies: Control when and how variables are applied during layer resolution
-
Documentation: Integrated help and validation error messages
For variable validation help and policy explanations, use or refer to the variable-validation help page accessible via the individual layer documentation pages.rpi-image-gen metadata --help-validation
For detailed information about a particular layer, including configuration options and defaults, please inspect the layer via the command line () or refer to the layer’s documentation page. It is recommended to use a config file to set layer variables. Layers that declare variables specify a defined prefix. Use this prefix in the config file to set variables applicable to that layer. For example - device and image layers define variables with prefix 'device' and 'image' respectively:rpi-image-gen layer --describe <layer name>
device:
storage_type: nvme
image:
compression: zstd
Device and Image Layers
The config system allows device and image layers to be specified two different ways. Both yield the same result. The main difference is that the latter allows the name of the variable holding the device/image layer to be defined by the user, therefore making it customisable. Using the former makes more sense if defining other device settings since they can all be encapsulated under the same section in the config file.
device:
layer: rpi5
layer:
myvar: rpi5
The above would result in two variables being defined:
IGconf_device_layer=rpi5
IGconf_layer_myvar=rpi5
Both would pull layer into the system configuration.rpi5
rpi-image-gen expands and references all variables at layer collection time, whereas it looks specifically for IGconf_layer_* and IGconf_device_layer to locate device and image layers respectively for those particular sections.IGconf_image_layer
It’s worth noting that rpi-image-gen does not mandate a device or image layer being specified. The construction of a filesystem can take place with or without either of these. For example, a user may wish to use rpi-image-gen to create a filesystem tar ball for use in a docker container.
Specifying Layers
Layers can be specified in the config file by name (i.e. their ). The name of the variable containing the layer name is completely arbitrary.X-Env-Layer-Name
layer:
foo: trixie-minbase
bar: rpi5
app: my-app
This would result in the following variables being defined:
IGconf_layer_foo=trixie-minbase
IGconf_layer_bar=rpi5
IGconf_layer_app=my-app
rpi-image-gen would attempt to locate layers ,trixie-minbase and rpi5, including their depdendencies. Deduplication of layers occurs at the resolution phase, meaning that specifying duplicate layer names is harmless and basically a nop.my-app
How to Use This Documentation
-
Browse the auto-generated layer list (currently only available in the HTML documentation) to find layers relevant to your build
-
Click on any layer name to view detailed documentation information including:
-
Configuration variables and their validation rules
-
Package dependencies and installation details
-
Layer relationships and dependencies
-
Technical implementation details and companion information
Getting Started
-
Choose a device layer that matches your Raspberry Pi hardware
-
Choose an image layer applicable to your deployment
-
Add a suite and/or list of general layers for additional functionality
-
Configure the variables as documented in each layer
-
Run
rpi-image-gen buildwith your config