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
crane
if 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.