Build a gadget snap

Gadget snaps define and manipulate device-specific configuration and system properties, such as partition layouts and default configuration options. Building a gadget snap enables these device-specific options to be set and integrated within an Ubuntu Core image.

This page includes an overview of what a gadget snap includes, plus templates for building the gadget and the snap.

See Gadget snaps for reference details on what gadget snaps can contain, and see Types of snap for details on the other types of snap that make up an Ubuntu Core image.

Gadget snap components

A gadget snap is built the same way other snaps are built, using snapcraft with a corresponding snapcraft.yaml.

For a general guide on building snaps, see Create a new snap.

A gadget snap’s snapcraft.yaml links to both the gadget.yaml, which is used describe the device, and the support, boot and configuration files that are necessary to initialise a device.

  • snapcraft.yaml defines the gadget snap metadata alongside build and boot parameters
  • gadget.yaml defines the system properties specific to one or more devices
  • grub/u-boot directories provide the device boot environment

The filesystem for a typical gadget snap looks like the following:

├── gadget.yaml
├── grub
│   ├── grub.builtin
│   └── uEnv.txt
├── snap
│   ├── hooks
│   │   ├── configure
│   │   ├── default-configure
│   │   └── prepare-device
│   └── snapcraft.yaml
└── u-boot
    └── boot.scr.in

Canonical’s IoT Devices Field team maintains a GitHub repository that includes template files in its main branch, and example gadget implementations for the following architectures in its branches:

Template: gadget.yaml

The following is an annotated gadget.yaml file that can be used as the basis for your own gadget snap:

gadget.yaml
# A template gadget.yaml
# For a full specification of the gadget.yaml, please see:
# https://snapcraft.io/docs/the-gadget-snap
volumes:
  <volume name>:
    schema: <mbr|gpt>
    bootloader: <u-boot|grub>
    structure:
      # Example of a bare structural item
      # Named 'BIOS boot' if using MBR
      - name: <descriptive name>
        type: bare
        offset: <if needed>
        size: <a multiple of 512>
        # Include an 'update' stanza, start at 1
        update:
          edition: 1
        content:
          - image: <file name to write to bare sectors>
      # Example of a system-seed partition
      - name: ubuntu-seed
        role: system-seed
        filesystem: vfat
        type: C12A7328-F81F-11D2-BA4B-00A0C93EC93B
        size: <twice the required size>
        offset: <if needed>
        # Include an 'update' stanza, start at 1
        update:
          edition: 1
        content:
          # GRUB vs u-boot
          - source: <grub<arch>.efi|boot.scr>
            target: <EFI/boot/grub<arch>.efi|/>
          # If using u-boot, you might include
          - source: uEnv.txt
            target: EFI/boot/uEnv.txt
      # Example of a system-boot partition
      - name: ubuntu-boot
        role: system-boot
        # Note that this partition cannot be vfat as a symlink to the kernel
        # file is created, and symlinks are verboten on vfat.
        filesystem: ext4
        type: 0FC63DAF-8483-4772-8E79-3D69D8477DE4
        size: <four times the kernel.{efi,img} size>
        # Include an 'update' stanza, start at 1
        update:
          edition: 1
        content:
          # GRUB vs u-boot - should match at least system-seed
          - source: <grub<arch>.efi|boot.scr>
            target: <EFI/boot/grub<arch>.efi|/>
          - source: uEnv.txt
            target: EFI/boot/uEnv.txt
          # If using u-boot, provide this file
          - source: boot.sel
            target: uboot/ubuntu/boot.sel
      # Example system-save partition
      - name: ubuntu-save
        role: system-save
        filesystem: ext4
        type: 0FC63DAF-8483-4772-8E79-3D69D8477DE4
        size: 32M
      # Example system-data partition
      - name: ubuntu-data
        role: system-data
        filesystem: ext4
        type: 0FC63DAF-8483-4772-8E79-3D69D8477DE4
        size: 1M
	
# Default configuration settings should go here

# Default connection settings should go here

Template: snapcraft.yaml

The following is an annotated snapcraft.yaml file that can be used as the basis for your own gadget’s snapcraft.yaml:

snapcraft.yaml
# A template snapcraft.yaml
# For a full specification of the snapcraft.yaml, please see:
# https://snapcraft.io/docs/snapcraft-yaml-reference

# Do not feel obligated to perfectly adhere to this style, but strive to include
# as much metadata as possible.

# The name should be generic -- do not leak secret information!
# A store prefix should be considered as secret information.
# The snaps here which include one will insert it during builds by using a
# secret consumed by the relevant workflow.
name: <platform>
# The version is largely human readable, but should be meaningful.
# For instance, if the device boots with GRUB, have the version match the GRUB
# release used.
adopt-info|version: <grub|u-boot|some useful string>
# The base of the snap should match the base of the Ubuntu Core system.
base: <base>
# build-base should only be used if creating a gadget for a devel release of Ubuntu Core.
type: gadget
# The grade should always be stable unless the gadget is using a devel-grade build-base.
grade: stable
# The confinement should always be strict.
confinement: strict
# The summary should be short and simple.
summary: An Ubuntu Core gadget for the <platform>
# Again: always include license information.
# Valid licenses: https://github.com/snapcore/snapd/blob/master/spdx/licenses.go
license: "CC-BY-SA-4.0 AND ..."
# This repository is upstream for this gadget. Optionally include your contact
# information.
issues: https://github.com/canonical/iot-field-gadget-snap/issues
website: https://github.com/canonical/iot-field-gadget-snap/tree/main
# The description should include boilerplate information and a description of
# any nuances with this gadget target platform.
# Always include license information.
description: |
  This snap provides the <bootloader> binaries for booting <platform>.

  Additionally it includes the gadget.yaml which defines the partition layout
  and any default configuration options for the Ubuntu Core device.

  ** Note that the license information of this snap is incomplete **
  Do your due diligence to ensure compliance with the licenses of the contents of this snap.

  The <text files you created> are licensed under CC-BY-SA-4.0

  You can find many licenses for the contents of this snap at the following locations:

  All provided: licenses/

# If you want the gadget.yaml to refer to assets provided by the kernel
# instead of this snap, use this key. See:
# https://snapcraft.io/docs/snapcraft-top-level-metadata
assumes: [kernel-assets]

# At a minimum the snap should build natively.
architectures:
  - build-on:  [<arch>]
    build-for: [<arch>]

# Any additional repositories required.
# For instance, if you are building on AMD64 for ARM64,
package-repositories:
  - type: apt
    components: [main]
    architectures: [<arch>]
    suites: [<release>, <release>-security, <release>-updates]
    key-id: F6ECB3762474EDA9D21B7022871920D1991BC93C
    url: http://ports.ubuntu.com/ubuntu-ports

# Please provide descriptive comments as necessary. Not everything needs to
# be documented or explained, but explaining why things are done can always help
# people who are learning!
parts:
  # If GRUB is specified as the bootloader in the gadget.yaml, then this
  # grub part is what should be used. bootloader in the gadget.yaml. The grub
  # part is used to fetch a GRUB binary from the Ubuntu archive. This package
  # should provide us everything needed for a gadget which specifies GRUB as a
  # bootloader. Additionally, the snap version information can be set from the
  # GRUB package used.
  # When the gadget uses GRUB, a file named grubenv will be created by snapd
  # during image build time in EFI/ubuntu and snapd will manage that file during
  # the lifetime of the device. This file is populated by snapd with important
  # system information, like the name of the seed system and the recovery mode
  # being booted with which tells snapd whether to install, run, or recover
  # Ubuntu Core.
  grub:
    plugin: dump
    # Examples of files required to use GRUB as the bootloader can be found in
    # the top-level grub directory
    source: grub/
    stage-packages:
      - grub-efi-<arch>:${CRAFT_ARCH_BUILD_FOR}
    override-build: |
      craftctl default
      
      # Set the version of the snap from the GRUB package
      craftctl set version=$(find "${CRAFT_PART_BUILD}/../stage_packages/" \
                              -name grub-efi-${CRAFT_ARCH_BUILD_FOR}_*.deb \
                              -exec dpkg-deb -f {} Version \;)

      # grub.conf is required when bootloader is set to GRUB in the gadget.yaml
      # so that snapd knows how to prepare the boot environment during image
      # build and during general run mode. The file should be empty.
      touch "${CRAFT_PART_INSTALL}/grub.conf"

      # uEnv.txt is a file that the u-boot binary will search for and source on
      # boot. This file should launch our grub<arch>.efi binary and result in
      # a fully automated boot process. No modifications are made to this file,
      # but it is provided by this project in the grub/ directory.
      # Note that the exact name and location of this file depends on the u-boot
      # being used by the target hardware.

    # Ship license information
    organize:
      usr/share/doc/grub-efi-<arch>/:     licenses/grub-efi-<arch>/
      usr/share/doc/grub-efi-<arch>-bin/: licenses/grub-efi-<arch>-bin/
    stage:
      - grub.conf
      - grub<arch>.efi
      - licenses/
      - uEnv.txt

  # u-boot provides the bootloader binary for the target hardware. This binary
  # is of paramount importance when u-boot is declared as the bootloader in the
  # gadget.yaml, but may also need to exist for other systems even when GRUB
  # is used as the bootloader. This is the binary the bootrom of the target
  # hardware should search for and execute after it performs its low-level
  # device initialization. With some platforms, a u-boot binary is provided from
  # the Ubuntu archives (the RISC-V platforms supported on Ubuntu Classic are
  # such an example). In other cases (as with many ARM64 platforms), u-boot must
  # be built from source. The character of this part will change dramatically
  # based on this fact.

  # u-boot-deb is the case where u-boot is provided by a package in the archive.
  u-boot-deb:
    plugin: nil
    stage-packages: [<package name>]
    override-build: |
      # Because only the u-boot binary is needed, simply copy it to the
      # top-level to reduce the path length for the binary in the gadget.yaml
      install -Dm644 "${CRAFT_PART_INSTALL}/<path to u-boot binary>" \
        "${CRAFT_PART_INSTALL}/<binary name>"

    # Ship license information
    organize:
      usr/share/doc/<package name>/: licenses/<package name>/
    stage:
      - <binary name>
      - licenses/

  # u-boot-src is the case where u-boot must be built from source.
  # Note that this may oftentimes mean that other parts need to be added to
  # build things the u-boot binary requires. For instance, a RISC-V board may
  # require also building an SPL and SBI binary which are then built into the
  # final u-boot binary. There could also be device trees which are encoded
  # in the binary, or some other bespoke file used by u-boot to determine
  # which files to load at run-time. This information should be covered in the
  # documentation for the target hardware or the u-boot source.
  # Examples of how to handle this can be found in other branches of this
  # repository. Specifically, branches for ARM64 devices will usually do this.
  u-boot-src:
    # A plugin could be utilized here; for instance, the make plugin may
    # be the correct choice. Because this selection may require some manual
    # intervention in the build process anyways, it may be preferable to simply
    # handle building and installing the part manually.
    plugin: nil
    source: <u-boot source tree>
    # The packages required to build u-boot may be variable; the documentation
    # for the u-boot source selected should include a list of dependencies.
    build-packages: [bc, make]
    override-build: |
      make <board>_defconfig
      make -j${CRAFT_PARALLEL_BUILD_COUNT}
      install -Dm755 "${CRAFT_PART_BUILD}/<path to u-boot.bin>" \
        "${CRAFT_PART_INSTALL}/u-boot.bin"

  # boot-sel provides a file snapd will look for just in case u-boot is chosen
  # as the bootloader in the gadget.yaml. This file is populated by snapd with
  # important system information, like the name of the seed system and the
  # recovery mode being booted with which tells snapd whether to install, run,
  # or recover Ubuntu Core.
  boot-sel:
    after: [u-boot]
    plugin: nil
    build-packages: [u-boot-tools]
    override-build: |
      # It is sufficient to create an empty file; the contents are owned by snapd.
      mkenvimage -r -s 4096 \
        -o "${CRAFT_PART_INSTALL}/boot.sel" - < /dev/null

      # uboot.conf is required when bootloader is set to u-boot in the
      # gadget.yaml so that snapd knows how to prepare the boot environment
      # during image build and during general run mode. The file should be
      # empty.
      touch "${CRAFT_PART_INSTALL}/uboot.conf"

  # boot-scr compiles the boot.scr.in file into an executable which u-boot can
  # execute at run-time. This script performs the necessary actions to boot an
  # Ubuntu Core system which declares u-boot as its bootloader.
  # A viable example of such a file can be found in the u-boot directory.
  boot-scr:
    after: [u-boot]
    plugin: nil
    source: u-boot/
    build-packages: [u-boot-tools]
    override-build: |
      mkimage -d boot.scr.in \
        "${CRAFT_PART_INSTALL}/boot.scr"

# Any necessary hooks. For instance, specify the API key here to be used
# by the prepare-device hook at runtime.
# If you don't know what an API key is, feel free to remove this.
# PLEASE do not check your API key into VCS :)
# The snaps here which include one will insert it during builds by using a
# secret consumed by the relevant workflow.
hooks:
  prepare-device:
    environment:
      MODEL_APIKEY: ""

Building the snap

There are typically two methods for building a gadget snap, cross building and native building. Cross building is likely the most convenient and performant as the gadget is built within a container on the host machine.

Cross building

Cross building allows a gadget to be built on an architecture different from the target. In the following example, we build for armhf on an amd64 host.

Prerequisites

  • The latest Ubuntu LTS release (recommended)
  • Snapcraft

First, either have your gadget directories prepared, or clone a pre-existing repository.

To use Canonical’s own repository, for example, you can clone the gadget repository and switch to the device branch for your device:

$ git clone https://github.com/snapcore/pi-gadget
$ cd pi-gadget
$ git checkout 24-armhf
[edit the gadget]
$ snapcraft
[...]
Snapped pi_24-1_armhf.snap

By default, snapcraft attempts to build the gadget snap in either an LXD (default) or Multipass container, isolating the host system from the build system. Both Multipass and LXD allow for the build architecture to differ from the run-on architecture, as defined by the architecture stanza in the snapcraft.yaml file for the gadget snap:

architecture
  - build-on: [amd64, armhf]
    run-on: armhf

The gadget snap can then be built by running the snapcraft command.

See Architectures for more details on defining architectures and Image building for instructions on how to build a bootable image that includes the gadget snap.

Native building

This method allows for the gadget snap to be built on the same hardware the gadget is intended for.

Prerequisites

To build the gadget snap:

  1. Install and set up LXD
  2. Launch a fresh instance of Ubuntu 24.04
  3. Within the instance:
    • Install snapcraft
    • Use either your own files, or a repository clone and brand
    • Build the gadget with snapcraft
  4. Exit the instance and obtain the snap from within the container

Running the following commands on the Raspberry Pi will perform the above process:

$ sudo snap install lxd
$ sudo lxd init --auto
$ sudo lxc launch ubuntu:24.04 noble
$ sudo lxc shell nobel
# snap install snapcraft --classic
# git clone https://github.com/snapcore/pi-gadget/
# cd pi-gadget
# snapcraft --destructive-mode
[...]
Snapped pi_24-1_arm64.snap
# exit
$ lxc file pull noble/root/pi-gadget/pi_24-1_arm64.snap .

See Image building for instructions on how to build a bootable image that includes the gadget snap.

2 Likes

you probably want to attach something like:

# exit
$ lxc file pull focal/root/pi-gadget/pi_*.snap .

to show how to obtain the built snap outside of the container …

2 Likes

Thanks - great suggestion. I’ve added this (and learnt that lxc file pull unfortunately doesn’t support wildcards).

3 Likes

Since the Pi gadget snap specifies building on either amd64 or arm64, is there a reason to check out the branch 20-armhf rather than 20-arm64?

No. I think at the time I wrote this, and tested it, there was a problem with the pi 64bit image. I’ll test it to make sure it now works and update this doc - thanks for flagging this.

Unsurprisingly, there’s a lot of “armhf” references throughout the docs, I’m guessing much of which can be upgraded to arm64.

“By default, snapcraft attempts to build the gadget snap in a Multipass container, isolating the host system from the build system.”

With Snapcraft 7 (at least for core 22), the default is now LXD, not Multipass.

Two more points.

First, with core22, “run-on” is now “build-for”.

Also, when building with LXD, you get the built snap outside the container automatically, no? So there is no need for “lxc file pull”.

“By default, snapcraft attempts to build the gadget snap in a Multipass container …”

These days, I think LXD is the default build provider. You should confirm that.

1 Like

If updating docs to UC22, all references to “run-on” should say “build-for”.

Thanks for the reminder. As I want to test this (UC22 and arm64), I’ve created a separate task to update this doc.

Lots of thoughts on this document; I think it needs a lot of love.

Firstly, I think breaking it up into “cross vs native building” is a bit contrived; it isn’t really the crux of the issue. The problem is more-so “how do I do the thing” in the first place; what does specifying grub as my bootloader mean, for instance. I didn’t know until about two weeks ago…

My proposal is a bit big, but:

Break up into page 1 & 2:
    - trivial:  stage the (reference) snap
         - it's easier, so here we can showcase 
           - including some new interface (pwm,gpio,custom-device), 
           - gadget.yaml should be large focus in this case
     - hard: build u-boot from source, construct boot.scr
           - annoying because we don't produce generic u-boot for ARM
           - upshot: we distribute u-boot payloads for RISC-V :)
           - really get into boot.scr, maybe a generic one (Field has several)

snapd’s grub implementation requires UKI (universal kernel image) so I think page 1 mentions exclusively GRUB, page 2 discusses exclusively u-boot.
No secureboot, no FDE. To complicated, bla.

Good news:

  1. We (Core) have reference gadgets which use GRUB, so the page can act as documentation for those reference gadgets
  2. We (Field) have reference gadgets which use u-boot, so the page can act as documentation for those (non-reference) gadgets – I plan to make Field’s gadget repository public Soon™.

Bad news:

  1. ??? I have to do some work, I guess?
1 Like