Building an APK Package with melange
Originally published in Japanese at https://zenn.dev/ymotongpoo/books/chainguard-image-toolchain/viewer/20-melange-handson.
Let’s confirm the mechanism from the previous chapter by actually running commands. melange is a single Go binary, but since building uses Linux namespace isolation, the easiest approach is to use the cgr.dev/chainguard/melange image, run on Docker. The steps that follow track the quickstart in melange’s README.
Preparing a signing key
APKs built with melange must be signed. First, generate a key pair in your working directory:
docker run --rm -v "${PWD}":/work cgr.dev/chainguard/melange keygen
This creates a private key, melange.rsa, and a public key, melange.rsa.pub, in the current directory. You’ll reuse this key for every build that follows.
Preparing a build definition
As Chapter 4 showed, the quickstart example in melange’s README uses Alpine Linux’s official repository as the build environment. In this book, though, we’ll align the build environment with Wolfi’s own repository, since Chapter 7 embeds the APK we build here into a Wolfi-based apko image. As confirmed in Chapter 3, Alpine Linux uses musl as its standard C library while Wolfi uses glibc, so a binary built in an Alpine environment can’t just be dropped into a Wolfi image as-is. Rewriting the README’s example for Wolfi gives you this:
package:
name: hello
version: 2.12
epoch: 0
description: "the GNU hello world program"
copyright:
- license: GPL-3.0-or-later
environment:
contents:
keyring:
- https://packages.wolfi.dev/os/wolfi-signing.rsa.pub
repositories:
- https://packages.wolfi.dev/os
packages:
- wolfi-base
- build-base
- ca-certificates-bundle
pipeline:
- uses: fetch
with:
uri: https://ftp.gnu.org/gnu/hello/hello-${{package.version}}.tar.gz
expected-sha256: cf04af86dc085268c5f4470fbae49b18afbc221b78096aab842d934a76bad0ab
- uses: autoconf/configure
- uses: autoconf/make
- uses: autoconf/make-install
- uses: strip
environment.contents assembles the build environment itself from Wolfi packages — the minimal wolfi-base from Chapter 3, build-base for the full build toolchain, and ca-certificates-bundle for HTTPS. pipeline then runs through fetching the source, building, installing, and stripping debug symbols, in order. autoconf/configure and autoconf/make are built-in actions that package up the equivalent of ./configure && make. ca-certificates-bundle is required — without it, the fetch action fails to verify the TLS certificate when talking to https://ftp.gnu.org.
Running the build
Build using the key and definition file you’ve prepared:
docker run --privileged --rm -v "${PWD}":/work \
cgr.dev/chainguard/melange build hello.yaml \
--arch x86_64 --signing-key melange.rsa
--privileged is needed for melange to set up its sandbox (bubblewrap by default) inside the container. On success, the build produces a signed hello-2.12-r0.apk, along with APKINDEX.tar.gz, the repository index file, under packages/x86_64/ in the current directory.
Inspecting the output
The generated .apk is, underneath, a tar archive containing signature information, metadata, and the actual set of files. You can inspect its contents with the tar command:
tar tzf packages/x86_64/hello-2.12-r0.apk
Alongside the executable, the package contains an SPDX-format SBOM file under var/lib/db/sbom/. As Chapter 4 mentioned, this SBOM is generated from inside the build process. Here’s an excerpt of the structure, taken from an actual SBOM (for the sed package) included in melange’s test data:
{
"SPDXID": "SPDXRef-DOCUMENT",
"name": "apk-sed-4.9-r8",
"spdxVersion": "SPDX-2.3",
"creationInfo": {
"creators": ["Tool: melange (devel)", "Organization: Chainguard, Inc"]
},
"documentDescribes": ["SPDXRef-Package-apk-sed-4.9-r8"],
"packages": [
{ "SPDXID": "SPDXRef-OperatingSystem", "name": "wolfi", "primaryPackagePurpose": "OPERATING-SYSTEM" },
{
"SPDXID": "SPDXRef-Package-apk-sed-4.9-r8",
"name": "sed",
"versionInfo": "4.9-r8",
"licenseDeclared": "GPL-3.0-or-later",
"primaryPackagePurpose": "APPLICATION"
},
{ "SPDXID": "SPDXRef-Package-Melange-testdata-buildC95configs-sed.yaml-c0ffee", "primaryPackagePurpose": "INSTALL" },
{ "SPDXID": "SPDXRef-Package-Source-git.savannah.gnu.org...", "primaryPackagePurpose": "SOURCE" }
]
}
Notice that the packages array is made up of four kinds of entries: the package itself (APPLICATION), the build definition that produced it (INSTALL), the source code it was fetched from (SOURCE), and its OS context (OPERATING-SYSTEM). Because this records exactly what was fetched and installed from inside the build process, it’s more accurate than the after-the-fact filesystem-scanning approach.
Adding the --generate-provenance flag outputs an SLSA-format provenance file alongside the build. If you’re building automatically in a CI environment, keeping this provenance information around lets you trace, later on, exactly which inputs a given package was built from.
Subpackages and tests
Adding subpackages to melange.yaml lets you produce a separate APK from the same build as the main package. For example, to split out a docs-only hello-doc, you’d write:
subpackages:
- name: hello-doc
pipeline:
- uses: split/manpages
Writing a verification pipeline in the test field also lets you validate the build’s output with the melange test command. The (excerpted) test definition for the crane package in melange’s examples is a clear example:
test:
environment:
contents:
packages:
- jq
pipeline:
- name: Verify Crane installation
runs: |
crane version || exit 1
crane --help
- name: Fetch and verify manifest
runs: |
crane manifest chainguard/static | jq '.schemaVersion' | grep '2' || exit 1
This verifies not just that the built crane command actually runs, but that it can talk to a registry and correctly fetch a manifest. This pattern is used widely across Wolfi’s repository, a large collection of package definitions.
In the next chapter, we’ll combine the hello APK built here with the APKs Wolfi publishes, and actually assemble a container image.