Writing your own buildpacks
The embeddedci-registry is the public repository that contains all buildpacks. You can contribute new packs by opening a pull request.
Registry structure
The registry organizes packs by category. Each pack is a directory containing a definitions.yaml and a scripts/ directory:
embeddedci-registry/
└── packs/
├── apps/
│ └── build/
│ ├── definitions.yaml
│ └── scripts/
│ └── build.sh
├── firmware/
│ └── stm32-build/
│ ├── definitions.yaml
│ └── scripts/
│ └── build.sh
├── system/
│ ├── kernel-linux/
│ ├── uboot/
│ ├── rootfs-image/
│ ├── initramfs-busybox/
│ ├── buildroot-kernel/
│ ├── buildroot-rootfs/
│ └── yocto-image/
├── images/
│ ├── bootfat/
│ └── sdcard/
└── test/
├── hardware/
└── qemu/The pack ID used in embeddedci.yaml is the path relative to packs/. For example, packs/firmware/stm32-build/ becomes firmware/stm32-build.
Anatomy of a buildpack
Every pack needs two files at minimum:
definitions.yaml— declares the pack's type, dependencies, config template, and artifacts.scripts/build.sh— the shell script that does the actual work.
definitions.yaml reference
The full schema with inline comments:
type: firmware # Pack type (see table below)
image: ghcr.io/... # Optional: Docker image to run the build in
scripts:
build: scripts/build.sh # Path to the build script (relative to pack dir)
run: scripts/run.sh # For test packs: the test runner script
default_vars: # Default values for template variables
my_var: default_value # Overridden by user's config in embeddedci.yaml
dependencies: # Sources and upstream pack artifacts to fetch
- pack: fetch # "fetch" = git clone a repo
dest: my_source # Directory name in build root
src: "{{my_repo}}" # Template referencing a var or config key
optional: true # Don't fail if src is empty
- pack: apps/build # Reference another pack's output
dest: "out/app-build/{{artifact}}"
optional: true
artifacts: # What this pack produces
- path: output/ # Fixed path (directory or file)
- path_var: artifact_bin # Path read from a config variable
path_prefix: "out/" # Prepended to the variable value
skip: # Rebuild optimization
check_paths: # Skip build if these paths already exist
- output/
config: # Template → env var mapping
my_var: "{{my_var}}" # Becomes MY_VAR in the build environment
build_root_path: "{{build_root}}/my_source"Pack types
| Type | Purpose | Examples |
|---|---|---|
firmware | Microcontroller firmware build | firmware/stm32-build |
app | Userspace application build | apps/build |
kernel | Linux kernel build | system/kernel-linux, system/buildroot-kernel |
boot | Bootloader build | system/uboot |
rootfs | Root filesystem generation | system/buildroot-rootfs, system/initramfs-busybox |
rootfs-image | Standalone rootfs image (ext4) | system/rootfs-image |
package | Combine artifacts into a bootable image | images/bootfat |
image-sdcard | Full disk image from partitions | images/sdcard |
yocto | Yocto/KAS-based image build | system/yocto-image |
test | Test runner (QEMU or hardware) | test/qemu, test/hardware |
Dependencies
Dependencies declare what a pack needs before it can run. There are two kinds:
Fetch dependencies
Clone a git repository into the build root. Use pack: fetch with a src template.
dependencies:
# Clone a git repo into the build root
- pack: fetch
dest: kernel_src # Directory name under build root
src: "{{repo}}?ref={{ref}}" # Git URL with optional ref
# Clone user's project source (optional — may not have src set)
- pack: fetch
dest: project_src
src: "{{src}}"
optional: truePack dependencies
Consume artifacts from another pack that ran earlier in the pipeline.
dependencies:
# Consume output from apps/build
- name: app_bin # Logical name (used in env_mapping for test packs)
pack: apps/build # Pack ID to depend on
dest: "out/app-build/{{artifact}}"
optional: true # Don't fail if apps/build wasn't in the pipelineConfig and template variables
The config section maps template variables to environment variables. Templates use {{variable}} syntax. The engine resolves them from (in priority order):
- User's
configinembeddedci.yaml - Pack's
default_vars - Board variables (prefixed
BOARD_) - Built-in variables:
{{build_root}},{{project_root}},{{arch_image_name}}
Config keys become uppercase environment variables in the build script. For example, my_lib_dir: "{{build_root}}/my_lib" becomes MY_LIB_DIR=/path/to/build/my_lib.
Artifacts
Declare what your pack produces so downstream packs and the engine can find the outputs:
# Fixed path — always produces this file/directory
artifacts:
- path: kernel
# Variable path — filename comes from user config
artifacts:
- path_var: artifact_elf
path_prefix: "out/stm32-build/"
# With build_path — artifact is at a different location during build
artifacts:
- path: rootfs.ext4
build_path: "buildroot_out/images/rootfs.ext4"Use path for fixed outputs, path_var when the filename depends on user config, and build_path when the file lives at a different location during the build.
Writing the build script
The build script runs inside the build environment with all config variables as uppercase env vars. Key environment variables available to every build script:
| Variable | Description |
|---|---|
BUILD_ROOT | Root directory for the build workspace (fetched sources, intermediate files). |
PROJECT_ROOT | Root of the user's project (where embeddedci.yaml lives). |
BOARD_ARCH | Target architecture (e.g. arm, arm64). |
BOARD_CROSS_COMPILE | Cross-compiler prefix (e.g. aarch64-linux-gnu-). |
BOARD_CC | C compiler (e.g. aarch64-linux-musl-gcc). |
BOARD_DEFCONFIG | Kernel/U-Boot defconfig for the target board. |
Example build script:
#!/bin/bash
set -euo pipefail
# Environment variables from config are available as uppercase:
# CMD, ARTIFACT_BIN, MY_LIB_DIR, PROJECT_DIR
# Board variables are also available:
# BOARD_ARCH, BOARD_CROSS_COMPILE, BOARD_CC, etc.
echo "==> Building with: $CMD"
# Use project source if fetched, otherwise fall back to PROJECT_ROOT
SRC_DIR="${PROJECT_DIR:-$PROJECT_ROOT}"
cd "$SRC_DIR"
# Run the user's build command
eval "$CMD"
# Copy artifact to the output location
ARTIFACT_DIR="$BUILD_ROOT/out/my-pack"
mkdir -p "$ARTIFACT_DIR"
cp "$ARTIFACT_BIN" "$ARTIFACT_DIR/"
echo "==> Build complete: $ARTIFACT_BIN"Writing test packs
Test packs use type: test and typically have a scripts.run instead of scripts.build. They include a test_config section with timeout, success patterns, and environment variable mapping from dependency names.
type: test
scripts:
run: scripts/run.sh
test_config:
timeout_sec: 30
cpu: cortex-a53
memory: 1024
boot_success_pattern: "Kernel: Linux"
env_mapping:
UBOOT_BIN: uboot_bin
BOOTFAT_IMG: bootfat_img
dependencies:
- name: uboot_bin
pack: boot/uboot
dest: u-boot.bin
- name: bootfat_img
pack: boot/bootfat
dest: bootfat.img
artifacts: []The env_mapping maps environment variable names to dependency name fields, so the run script can find artifact paths via environment variables.
Complete example
Here's a complete custom pack that builds firmware with a custom library:
definitions.yaml
type: firmware
scripts:
build: scripts/build.sh
default_vars:
my_lib_repo: https://github.com/example/my-lib.git
my_lib_ref: v1.0.0
dependencies:
- pack: fetch
dest: project_src
src: "{{src}}"
optional: true
- pack: fetch
dest: my_lib
src: "{{my_lib_repo}}?ref={{my_lib_ref}}"
artifacts:
- path_var: artifact_bin
path_prefix: "out/my-pack/"
config:
src: "{{src}}"
cmd: "{{cmd}}"
artifact_bin: "{{artifact_bin}}"
my_lib_dir: "{{build_root}}/my_lib"
project_dir: "{{build_root}}/project_src"scripts/build.sh
#!/bin/bash
set -euo pipefail
# Environment variables from config are available as uppercase:
# CMD, ARTIFACT_BIN, MY_LIB_DIR, PROJECT_DIR
# Board variables are also available:
# BOARD_ARCH, BOARD_CROSS_COMPILE, BOARD_CC, etc.
echo "==> Building with: $CMD"
# Use project source if fetched, otherwise fall back to PROJECT_ROOT
SRC_DIR="${PROJECT_DIR:-$PROJECT_ROOT}"
cd "$SRC_DIR"
# Run the user's build command
eval "$CMD"
# Copy artifact to the output location
ARTIFACT_DIR="$BUILD_ROOT/out/my-pack"
mkdir -p "$ARTIFACT_DIR"
cp "$ARTIFACT_BIN" "$ARTIFACT_DIR/"
echo "==> Build complete: $ARTIFACT_BIN"Reading existing packs
The best way to learn how packs work is to read the existing ones. The registry is public — browse the packs/ directory to see how each pack is structured:
packs/firmware/stm32-build/— STM32 firmware build with HAL/CMSIS fetching and Docker image.packs/apps/build/— Simple app build, good starting point for a custom pack.packs/system/kernel-linux/— Kernel build with defconfig merging and board variable integration.packs/system/rootfs-image/— Rootfs generation with user management and app staging.packs/test/qemu/— QEMU test runner with dependency wiring via env_mapping.
Contributing a new pack
- Fork the embeddedci-registry repository.
- Create your pack directory under
packs/<category>/<name>/. - Add
definitions.yamlwith type, dependencies, config, and artifacts. - Add
scripts/build.sh(make it executable withchmod +x). - Test your pack locally using the EmbeddedCI client.
- Open a pull request with a description of your pack and an example
embeddedci.yaml.
PR checklist
- Pack lives under
packs/<category>/<name>/ definitions.yamlhas type, scripts, dependencies, artifacts, and configscripts/build.shis executable- Build script uses
set -euo pipefail - All config keys use template variables (
{{var}}) - Default values provided in
default_varswhere sensible - Tested locally with the EmbeddedCI client
Further reading
- Buildpacks reference — all available packs and their config options.
- Configuration reference — full
embeddedci.yamlschema.