Skip to content

A tool to generate highly customised software images for Raspberry Pi devices.

License

Notifications You must be signed in to change notification settings

raspberrypi/rpi-image-gen

Repository files navigation

rpi-image-gen

rpi-image-gen is a tool used to create custom software images for Raspberry Pi devices. rpi-image-gen runs best on a Raspberry Pi Host running up-to-date 64-bit Raspberry Pi OS.

For the tool used to create Raspberry Pi OS, please go to https://github.com/RPi-Distro/pi-gen

NOTE rpi-image-gen is under active development. Please report issues at https://github.com/raspberrypi/rpi-image-gen. Feature suggestions are welcome.

Terminology

The following terms are used in this document:

Host The machine on which rpi-image-gen is executing. Analogous to GNU Build.

Device The machine on which the image generated by rpi-image-gen will execute. Analogous to GNU Host.

Class The device hardware family that the software image is built for.

Variant Enables differentiation between devices of the same class.

Package A collection of files and software resources, encapsulated in a known format, that constitute part of a larger software system.

Config A file that defines the device rootfs profile, disk image configuration identifier and top-level attributes.

Profile A file that contains the YAML layers for the device rootfs.

Image Identifies the configuration used to create the device disk image file.

Layer A single file of YAML metadata.

Meta A directory containing YAML layers in a defined hierarchy.

Hook A shell script that may be optional and which if found, will be run at a defined point in a particular flow of execution.

SBOM Software Bill Of Materials

CVE Common Vulnerabilities and Exposures

TL;DR

git clone https://github.com/raspberrypi/rpi-image-gen.git
cd rpi-image-gen
sudo ./install_deps.sh
./build.sh

The device image will be in work/deb12-arm64-min/artefacts/deb12-arm64-min.img

Install it onto an SD card using Raspberry Pi Imager (https://www.raspberrypi.com/software/). Select "Use Custom" if using the GUI. If using the command line and /dev/mmcblk0 is where you want to install to:

sudo rpi-imager --cli work/deb12-arm64-min/artefacts/deb12-arm64-min.img /dev/mmcblk0

Note: The default image intentionally has login passwords disabled. Please look in the examples directory to help understand how to customise the installation of packages and make your own modifications.

Why use this?

  • Quick to build. You don’t have to build the whole world from source.

  • By using the same binaries as millions of people are using in production worldwide, you know you’ve not accidentally inserted vulnerabilities with your choice of tool chain, optimisations, build flags, etc.

  • Use the same version set of libraries and applications as you do on Raspberry Pi OS. You won’t have to install a bleeding edge version of a library to build your application.

  • Configure your filesystem exactly how you want it and use rpi-sb-provisioner (https://github.com/raspberrypi/rpi-sb-provisioner) to automatically setup and enable signed boot and encrypted filesystems.

  • Output your software bill of materials (SBOM) to list the exact set of packages that were used to create the image.

  • Generate a list of CVEs identified from the SBOM and give consumers of your image confidence that your image does not contain any known vulnerabilities.

Getting started

git clone https://github.com/raspberrypi/rpi-image-gen.git

Dependencies

To install the required Host dependencies for rpi-image-gen you should run:

cd rpi-image-gen
sudo ./install_deps.sh

The file depends contains a list of the dependencies required. The format of this file is <tool>[:<debian-package>].

Overview

rpi-image-gen is a root file system and image creation tool designed with a high degree of customisation, flexibility and control in mind. rpi-image-gen uses bdebstrap (https://github.com/bdrung/bdebstrap) and mmdebstrap (https://gitlab.mister-muffin.de/josch/mmdebstrap) for rootfs construction and genimage (https://github.com/pengutronix/genimage) for image creation. rpi-image-gen is a Bash orientated scripting engine capable of producing software images with different on-disk partition layouts, file systems and profiles using collections of metadata and a defined flow of execution. It provides the means to create a highly customised software image for your Raspberry Pi device. rpi-image-gen is human readable, auditable and easy to use.

A device system is created using the concept of 'collections' where a collection is some YAML metadata in a defined format that describes a list of packages to be installed in, and operations to be performed on, the chroot (i.e. the device rootfs). Multiple YAML layers can be grouped into customisable profiles with all layers being aggregated and merged by bdebstrap. Device and image layout hooks are supported which enable a user to do things like apply a static rootfs overlay directory hierarchy specific to their device, perform custom post-build operations, integrate with third-party build systems, and have complete control over the input genimage configuration file(s) used to produce the device disk image(s).

rpi-image-gen heavily favours a Debian-ish installation process where a temporary apt configuration is assembled and used to seed the chroot. However, unlike debootstrap, mmdebstrap provides many more configuration options which can be defined in the YAML layers read by bdebstrap. This make it possible not only to describe different variants of the base rootfs in a modular way (e.g. ranging from a 'standard' Debian install with Essential packages to a rootfs that consists of nothing more than busybox, libc and a few basic utils), but also to use multiple mirrors in which to retrieve packages, and be able to perform customisable operations at defined stages of chroot creation, e.g. at 'setup', 'extract', 'essential', 'customize' and 'cleanup' points.

rpi-image-gen runs as a regular user and does not require root privileges.

Directory Hierarchy

The rpi-image-gen tree structure contains individual sub-component directories, each of which has a specific purpose in the execution flow. Certain directories may be of particular interest when integrating with rpi-image-gen because it’s possible to set their location to within an externally provided directory. Doing so would, for example, enable a user to:

  • Use one of the in-tree image layouts, but use their own device directory (e.g to apply a custom product specific roofs overlay, execute hooks to perform bespoke operations).

  • Augment the YAML layer search with a directory of their choice (e.g. where they own all layers or to integrate with a third-party).

  • Define their own image partition layout, file system types, mount points, etc.

  • Use a custom profile which contains a combination of in-tree layers and/or their own (e.g. customise the packages to install, perform specific chroot operations, etc).

EXT_DIR

If rpi-image-gen is provided with a path to an external directory on the command line it will use this directory when resolving sub-components paths for config/, profile/, image/ and device/ and it will include the meta/ sub-directory of this path in the list when searching for YAML meta layers defined by the Profile. This allows a user to provide external directories for these sub-components and for them to take precedence over the in-tree equivalents. The external directory does not need to contain all sub-components; it may just contain custom hooks. If no external directory is provided, variable IGTOP defines the root directory of all sub-components.

config

The config file is provided as a command line argument and is loaded from the external directory or in-tree. The config directory contains rpi-image-gen config files. Once the config file is loaded and parsed, sub-components for device, profile and image are searched for and their paths resolved, either from the external directory or in-tree. rpi-image-gen will always prioritise the external directory when searching. If not specified, a default config file is used. See ./build.sh -help for further information.

device

A device directory contains assets which are specific to a class of hardware device. Additionally, rpi-image-gen adopts a sub-directory named device in various locations to denote the contents are related to run-time software on the device (e.g. rootfs-overlay).

image

The image directory contains the necessary assets with which rpi-image-gen will use to create the output disk image(s) with a particular layout.

meta

The meta directory contains YAML layers which are individual 'recipes' used for rootfs creation. The search path for additional meta layers can be augmented by usage of EXT_DIR and optional namespace.

profile

The profile is a plain text file which supports comments via # and where each line contains a YAML layer. The profile directory contains rpi-image-gen profiles. For example, if a Profile contained:

# My device layer
my/fantastic/layer

…​my/fantastic/layer.yaml would be searched for.

sbom

The SBOM directory contains assets specific to generation of the SBOM (see below).

Other sub-component directories exist for particular purposes and the path to some of those are propagated to all layers via dedicated variables. These are mainly to assist with scripting and template generation, e.g. when creating a systemd config fragment for a network interface, creation of RPi boot firmware config files, etc.

Options

rpi-image-gen settings are aggregated as part of the input parameter assembly stage (see below) before commencing the build, and are translated into fully qualified variables with prefix IGconf. Settings specific to a sub-component are denoted by a particular namespace prefix such as device or image. Default settings specific to a sub-component are loaded from files named 'defaults' in the appropriate directory (e.g. build.defaults). rpi-image-gen uses the appropriate sub-component namespace when reading these files which simplifies variables declared within. All of these settings are classed as 'options' or 'configuration variables' depending on the scope. A variable can be set by:

The Environment
The config file (-c)
The user options file (-o)
Default settings

Rather than listing all available options here, please refer to the following files in the tree for further details:

device/build.defaults

Device configuration build-time settings, e.g. class, profile, locale
Auto-prefix: device

image/build.defaults

Default build options for all image layouts, e.g. version, compression
Auto-prefix: image

sys-build.defaults

Default options for the build configuration, e.g. APT key directory, output artefacts directory
Auto-prefix: sys

sbom/defaults

Default options for SBOM generation, e.g. output format
Auto-prefix: sbom

meta/defaults

Default options for all meta layers
Auto-prefix: meta

Meta layers can also provide their own specific default settings. Files declaring these defaults are located alongside their corresponding YAML layer and are loaded if the profile contains the associated layer.

Execution Flow

The three main stages of execution in rpi-image-gen are described below.

Input Parameter Assembly

Before commencing creation of the rootfs, the configuration of the system is assembled and translated into a set of environment variables established from different sources in the following order:

  • User option file settings (scope: explicit set and unset [see Note1 below])

  • Config file settings (scope: aggregate, set and unset)

  • System wide default settings (scope: aggregate, set and unset)

  • Selected Device and Image settings (scope: aggregate, set and unset)

  • User option file settings (scope: explicit set and unset)

The config file uses .ini file syntax (https://en.wikipedia.org/wiki/INI_file) to set key=value pairs inside a section.
The user options file, and files containing default settings, are shell fragments containing declarative key=value pairs.

Both file types support comments, and empty lines are ignored.

The loading of defaults settings or the config file will only set a variable if it is unset, i.e. their assignments are 'aggregated'. The user options file can override any variable. For example, if variable IGconf_image_name was set in the Environment it would not be set by the loading of image defaults or the config file. It would, however, be set if the user options file assigned image_name to a non-empty string. If the user options file assigned it as empty it would be unset. When the declarative syntax is parsed, it does not support setting a variable to be empty. If a variable is assigned as empty it will be treated as being unset. Because of this, the user options file is re-read at the end of parsing to ensure it is able to override any previously set variables to be unset if necessary.

As mentioned above, rpi-image-gen automatically applies a global prefix for all variables it parses from files and it applies a scope specific prefix to variables declared in config files and defaults. This 'translation' of variable parsing ensures variables automatically adhere to their defined scope. Unsupported config file sections are ignored. Variables set from the options file will be IGconf prefixed and translated as-is. Translation includes the variable name being converted to lower case. A variable is said to be 'fully qualified' after parsing and when prefixed with IGconf. All fully qualified variables are propagated and made available to YAML layers and hooks via their execution environment.

Note1: rpi-image-gen variables set from the Environment must be fully qualified otherwise they will be ignored. For example, to set the Device Class from the Environment, set variable IGconf_device_class accordingly. To set it from the user options file, assign variable device_class.

Note2: Exceptions to this are the individual meta layer default files which are loaded at the same time as their corresponding YAML layer.

Understanding how to set and create variables is an important part of using rpi-image-gen because it forms the foundation of user customisation.

Sections

The following sections are supported when parsing a config file:

  • device

  • image

  • sys

  • sbom

  • meta

Variables declared outside the scope of these sections will not be read.

Example 1

These are equivalent:

Default

IGconf_device_class=pi5

Config

[device]
class=pi5

Options

DEVICE_CLASS=pi5

Processing the input sources and aggregating the variables this way allows the setting of a variable to override a previous setting. This may be particularly useful where customisation may only require the modification of a small number of variables compared with what was set previously. Furthermore, rpi-image-gen evaluates each variable after translation which means that variables set previously can be used to set other variables.

Example 2

Default

IGconf_device_class=pi5
IGconf_image_suffix=img

Options

image_suffix=${IGconf_device_class}.bin

Yields:

IGconf_image_suffix=pi5.bin
Tip

Via the Options file or a supported Config file section, it is possible to define new custom variables. rpi-image-gen does not 'filter' variables or perform any sort of manipulation of their values/contents. The propagation of all variables, including user defined custom variables, may be beneficial to YAML layers and hooks.

Example 3

Environment

IGconf_device_user1=pi

Config

[device]
user1=rasp

[sys]
flavour=debug
debugger=autoattach
debug_port=8080

Options

sys_debug_port=
sys_debug_user=$IGconf_user1
sys_debugger=disabled

Yields

IGconf_sys_flavour=debug
IGconf_sys_debugger=disabled
IGconf_sys_debug_user=pi
Tip

It’s possible to include a Config file from another Config file provided both exist in the same directory. This may be useful in cases where there is a common configuration shared across variants. Rather than duplicating parts of a Config file in others, a common configuration can be included and any overrides or additional settings applied afterwards.

Example 4

Common configuration fragment base.cfg

[device]
class=edge01
profile=v1

[image]
layout=ROTA
default_slot=A

[sys]
safe_boot=y
boot_scheme=2

A developer may wish to use customisations provided by the layout

include base.cfg

[image]
resize=false
appdata_size=1G

Note: Files must be included outside a config section. Including files from the user options file is not supported.

Root File System Construction

After assembling the environment variables from the input sources, rpi-image-gen reads the Profile and validates the path of each YAML layer before reading its default settings (if present) and assembling each layer as an argument to bdebstrap. A layer must adhere to bdebstrap YAML syntax. Please refer to the bdebstrap man page for further details. It’s also worth pointing out that if authoring shell expressions in YAML, it may be necessary to adopt usage of particular block scalar styles to achieve newlines inside a block. For example:

  - |-
    chroot $1 bash -- <<- EOCHROOT
    source /opt/device/setup.sh
    run_provisioning
    EOCHROOT

In addition to each YAML layer, all IGconf variables are also passed to bdebstrap as arguments. This enables access to these variables from all layers.

A number of 'core hooks' are installed via bdebstrap command line arguments. These are run in addition to shell operations specified via by the YAML and provide the means for rpi-image-gen to execute common hooks at particular points in the creation of the chroot. They also serve the purpose of providing functionality at defined points that a custom layer(s) may need. For example, it may be desirable for all initramfs kernel images installed in the chroot to be rebuilt before the device file system images are generated. It’s unlikely that this operation needs to be done more than once during chroot creation, so there is a hook that runs update-initramfs. This removes the need for a layer to invoke this command specifically and it helps to reduce potential complications that could arise depending on the aggregation order of layers. Because bdebstrap prioritises hooks which are provided as command line arguments, their execution point in the flow is deterministic.

rpi-image-gen runs bdebstrap in a new Linux namespace via podman unshare. This is required so that bdebstrap creates files with the correct ownership information, which is particularly important when creating a chroot that contains a user account. See podman-unshare(1) and user_namespaces(7) for further details.

Before bdebstrap invokes mmdebstrap to begin creation of the chroot, it writes the fully aggregated and merged YAML to $IGconf_sys_outputdir as config.yaml. This is an incredibly useful file because it’s essentially the 'recipe' for generating the chroot. It also creates a file called manifest in the same directory which lists all the packages that were installed in the chroot.

The mmdebstrap execution is not regarded as in the scope of this document. mmdebstrap follows the rules governed by its design and by the configuration provided to it by bdebstrap. In turn, the vast majority of bdebstrap configuration is derived from the YAML layers provided to it by rpi-image-gen. From this point of view, rpi-image-gen could be regarded as a thin toolkit wrapper with which to design a system purely from YAML constructs and a set of environment variables. The application of both these things, once understood, is very powerful and provides the means to create a completely customised rootfs.

For example, it is possible to create a usable, minimal chroot with only the following YAML layer:

---
name: bookworm-arm64-svelte
mmdebstrap:
  architectures:
    - arm64
  mode: auto
  variant: custom
  suite: bookworm
  mirrors:
    - deb http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware
    - deb http://deb.debian.org/debian-security bookworm-security main contrib non-free non-free-firmware
    - deb http://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware
  packages:
    - dpkg
    - busybox
    - libc-bin
    - base-files
    - base-passwd
    - debianutils

SBOM

A SBOM should be a reproducible and immutable artefact of the build. Generation of it takes place at the end of the post-build stage after all other hooks have run, and immediately before genimage invocation. It is not advised to perform any operation on the rootfs that could affect the data encapsulated by the SBOM after it has been generated. The SBOM is generated using syft (https://github.com/anchore/syft) which will be downloaded if not installed on the host. The SBOM is written to the image output directory (IGconf_sys_outputdir) and its output format is configurable via the SBOM_OUTPUT_FORMAT variable which can be set via the Config or Options file. The output format is currently the only option available to the user.

For example, these are equivalent:

Config

[sbom]
output_format=cyclonedx-json

Options

SBOM_OUTPUT_FORMAT=cyclonedx-json

The value of IGconf_sbom_output_format is passed directly to syft. Please refer to the syft documentation for the supported output formats. Nothing in the rootfs is excluded from the SBOM scan. syft is invoked via podman unshare so it can execute with the same privileges as bdebstrap did when the rootfs was created.

Utilisation of the SBOM is beyond the scope of this document. However, the following is offered as an example of how the SBOM could be used to generate a report of known CVEs using grype (https://github.com/anchore/grype):

$ grype --by-cve sbom:./work/deb12-arm64-min/artefacts/deb12-arm64-min.sbom -o json > vulnerabilities.json

Image Generation

After the chroot is successfully created (i.e. 'post-build'), file system overlays are applied (if present) before a number of hooks are run. These hooks provide additional integration points and provide a powerful way to perform custom operations on the chroot before image generation commences. One of hooks that runs at this point is the pre-image hook. Similar to bdebstrap invocation, every hook is invoked in an environment where all IGconf variables are available. Hooks are also invoked in the directory in which they exist, which means they can use relative paths to assets and sub-directories specific to their function.

The pre-image hook is called by the core engine with defined arguments such as the path to the chroot, and the path to the genimage input directory. It is the responsibility of this hook to perform the tasks required to create the file(s) (i.e. the templates) in the genimage input directory so that genimage can process them to create the device image(s). Failure to generate a file(s) of the expected naming convention (genimage*.cfg), or to use syntax that renders the file(s) unable to be read by genimage, will result in an error and no device image will be generated. Please refer to the genimage documentation for usage information and examples of creation templates, parameters, etc.

When invoking genimage, rpi-image-gen sets the value of --inputpath and --outputpath to the same location so that it’s possible to reference one image from another. For example, image1 may be a squashfs image containing various assets that needs to be present on a file system in image2 so it can be mounted at boot.

rpi-image-gen is again somewhat of a thin toolkit wrapper, this time leveraging genimage functionality with its documented arguments and parameters. Like bdebstrap, genimage is run via podman unshare which is necessary to enable file system creation utilities to incorporate correct and valid file system metadata from the chroot when creating the partition images, before assembling them into the final device image(s).

Overlays

An 'overlay' is a statically defined directory tree hierarchy that is copied into the chroot after bdebstrap completes execution. An overlay is identified by a directory named 'rootfs-overlay' and can reside in two independent locations:

  • <image dir>/device/rootfs-overlay (Usage: optional) (Provided-by: image)

    Image layout specific root file system contents.
  • <device dir>/device/rootfs-overlay (Usage: optional) (Provided-by: device)

    Device specific root file system contents.

Overlays are applied in the order they are listed above.

Hooks

The hooks available for user customisation are documented below. If a hook is to be executed, it must have executable permissions for the user performing the build.

bdebstrap

rpi-image-gen extends the support of bdebstrap hooks to image, device and external (EXT_DIR) directories. Hooks with filenames beginning with setup, essential, customize and cleanup are supported and must exist in a sub-directory namedbdebstrap within the directory in order for them to be run at the respective stage of chroot creation. Their file extension is ignored. Sub-directories are not traversed.

initramfs

If initramfs-tools(7) is installed in the chroot, rpi-image-gen extends the support of initramfs scripts and hooks to image and device directories via their sub-directory device/initramfs-tools. If present, the entire contents of this directory is recursively copied into the chroot. Mode and ownership attributes are preserved. Destination files will not be overwritten. rpi-image-gen performs this operation during the customize stage of chroot creation and guarantees it will take place after invocation of all image and device bdebstrap customize hooks.

pre-build

  • <image dir>/pre-build.sh (Usage: optional) (Provided-by: image)

    Image layout specific pre-build operations.
  • <device dir>/pre-build.sh (Usage: optional) (Provided-by: device)

    Device specific pre-build operations.

Both hooks are executed in the order they are listed above.

post-build

  • <image dir>/post-build.sh (Usage: optional) (Provided-by: image)

    Image layout specific post-build operations.
  • <device dir>/post-build.sh (Usage: optional) (Provided-by: device)

    Device specific post-build operations.

Both hooks are executed in the order they are listed above.

pre-image

  • <device dir>/pre-image.sh (Usage: Mandatory - see notes below) (Provided-by: device)

    Device owned pre-image generation operations.
  • <image dir>/pre-image.sh (Usage: Mandatory - see notes below) (Provided-by: image)

    Image layout owned pre-image generation operations.

Only one of these hooks is executed. The device pre-image hook has priority. If it exists, it will be executed, else the image layout pre-image hook will be executed.

post-image

  • <device dir>/post-image.sh (Usage: Optional - see notes below) (Provided-by: device)

    Device owned post-image operations.
  • <image dir>/post-image.sh (Usage: Optional - see notes below) (Provided-by: image)

    Image layout owned post-image operations.

Only one of these hooks is executed. The device post-image hook has priority. If it exists, it will be executed. If it doesn’t and the image layout post-image hook exists, that will be executed. If neither exist, a default post-image hook will be executed. It is the responsibility of the post-image hook to perform all operations required to deploy artefacts to IGconf_sys_deploydir, for example compressing image and SBOM.

About

A tool to generate highly customised software images for Raspberry Pi devices.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages