Tuan-Anh Tran
January 13, 2024

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.

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:

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

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.

Follow me

Here's where I hang out in social media