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.