Writing your first Wolfi package
Posted on January 13, 2024 • 6 minutes • 1155 words
The Write your first Wolfi package contributing guideline on Wolfi repo is a bit vague for beginner so I thought a more detailed, hands-on tutorial would benefit first-time contributor.
Local dev environment
The first step of contributing is to setup a build environment locally.
Thanksfully, Wolfi team makes it very easy by just running make dev-container from the root of the repo. This assumes that you already have Docker installed.
From here on, let’s assume you’re already inside the dev-container.
How to build an existing package
Each of the package in Wolfi repo consists of 2 things. Let’s say we have a package name crane. I pick crane because it’s a very typical Go project and relatively easy to build.
- a YAML file
crane.yaml - and an OPTIONAL folder of the same name
craneif you have any patches. Any files you have in this folder will be copied in the workspace of the build environment.
To build the package, you just have to run make package/<pkg-name> and in this case, it will be make package/crane.
That’s how you can build a package locally.
In the next part, I will go into detail of how you can package one.
Writing your first Wolfi package
Let’s take a look at crane.yaml
Basic package information
package:
name: crane
version: 0.17.0
epoch: 3
description: Tool for interacting with remote images and registries.
copyright:
- license: Apache-2.0
dependencies:
runtime:
- ca-certificates-bundle
The first section is the basic information about the package: name, version , epoch, description, etc.. Those are pretty self-explainatory. The only thing you need to note here is epoch field. You need to increase epoch in case you are re-building the same version of the software like:
- bumping deps to fix CVEs.
- fix a packaging issue.
- etc…
When you bump version of the package itself, make sure you reset the epoch back to 0.
The dependencies block describes what kind of package you need as runtime dependencies for your package. For example, crane says here that it needs ca-certificates-bundle as runtime dependencies.
Build environment
Let’s move on to the next block environment.
environment:
contents:
packages:
- busybox
- ca-certificates-bundle
- go
environment:
CGO_ENABLED: "0"
This one here let you describe what’s needed to setup build environment. In our case, it’s a go package so you need go obviously, busybox for some basic commands like ln, mv and stuff like that and ca-certificates-bundle since you need to download go deps.
The environment.environment block lets you define any environment variables if you have any.
Build pipeline
This here is obivously the most important part of the package manifest. If you’re familiar with GitHub Actions, you may find it very similar.
pipeline is an array of build step. Each one of them is either a runs block that looks like below. It let you define any abitrary commands you want to run.
pipeline:
- runs: |
export CFLAGS="$CFLAGS -D_GNU_SOURCE"
./configure \
--build="$CBUILD" \
--host="$CHOST" \
--prefix=/usr \
--mandir=/usr/share/man \
--sbindir=/sbin \
--sysconfdir=/etc \
--without-kernel \
--enable-devel \
--enable-libipq \
--enable-shared
Or it can be an uses block which let you use a built-in step of melange. The list of the all the built-in steps can be found in melange repo here
.
Some of the most common one you will use are
fetch: which let you download from an URLgit-checkout: which let you clone from a git repo. You can use this one in case you want to add Git commit hash to your build command for example.autoconf/*: if the package useautoconf.- and many more language specific package like
go/build, etc…
There are many pre-defined variables which you can use throughout your pipeline like:
${{package.name}}
${{package.version}}
${{package.full-version}}
${{package.epoch}}
${{package.description}}
${{targets.destdir}}
${{targets.contextdir}}
${{targets.subpkgdir}}
${{host.triplet.gnu}}
${{host.triplet.rust}}
${{cross.triplet.gnu.glibc}}
${{cross.triplet.gnu.musl}}
${{build.arch}}
One of the most important one you should take note is the ${{targets.destdir}} one. If you want a file to appear in your package tar, this is where you should add it.
As @kaniini helps pointing out that ${{package.contextdir}} is a new better var, which were recently added Aug 2023
.
This is intended to be used as a destination target instead of
${{targets.destdir}}and${{targets.subpkgdir}}, allowing pipelines to be more reusable in subpackages, without hacks like ${{inputs.subpackage}}.
For example, after build, my binary is located at ./dist/my-binary, I will need to use this command below to add it to my package and have it installed at /usr/local/bin
mkdir -p ${{targets.contextdir}}/usr/local/bin/
mv ./dist/my-binary ${{targets.contextdir}}/usr/local/bin/my-binary
You can also perform a quick check with tar to see what files your packages have with this
tar --list -f /path/to/the/package/apk
Subpackages
It’s common practices where you will split up your packages into several packages to reduce the size of the main package. For example, you can split documentation into a subpackage like my-pkg-doc.
This section looks pretty much like the main package’s pipeline.
- name: bind-dnssec-tools
dependencies:
runtime:
- bind-tools
- libgcrypt
- libxml2
- curl
pipeline:
- runs: |
mkdir -p ${{targets.subpkgdir}}/usr/bin
mv \
${{targets.destdir}}/usr/bin/nsec3hash \
${{targets.destdir}}/usr/bin/dnssec* \
${{targets.subpkgdir}}/usr/bin/
description: Utilities for DNSSEC keys and DNS zone files management
Auto-udpate
One of the selling point of Wolfi is they have a lot of automation built around the repo. One of it is this update section.
In here you can define how you can hint the wolfictl bot to automate updating the package. It can either be from GitHub, Release Monitor or other sources. You can find out more in wolfictl repo
update:
enabled: true
release-monitor:
identifier: 242117
Testing the package
I usually just use apko to test installing the package I recently build. You can create a file named myimage.apko.yaml with the following content:
contents:
keyring:
- https://packages.wolfi.dev/os/wolfi-signing.rsa.pub
repositories:
- https://packages.wolfi.dev/os
- '@local ./packages'
packages:
- wolfi-baselayout
- busybox
- bash
- my-package@local
entrypoint:
command: /bin/sh
archs:
- amd64
You will need to run melange keygen command once to generate the keypair.
From the dev-container, you can simply run the following command
apko build \
--keyring-append /etc/apk/keys/wolfi-signing.rsa.pub \
--keyring-append ./local-melange.rsa.pub --arch host \
myimage.apko.yaml image:tag image.tar
# docker load -i image.tar
# docker run -it --rm image:tag-amd64
Once it’s done, you can use docker to load it and run it.
Where do I start?
Scan Wolfi repo for issue with label [wolfi-package-request] is how I usually do.
Or you can start with the package that you actually need.
For reference build instruction, I would usually look at other packages within the Wolfi repo as well as the Alpine package registry. The Alpine package repository provides excellent instruction on how packaging should be done.
You can take those as reference and start converting it into melange package.
Here’s the link to Alpine packages registry
. You can search for the package, click on the link to go to package page. And then go to Git repository of that package. From there, click to view the APKBUILD file.

If you struggle with any of the build, just open an issue and I’ll be happy to help you out.