The Ur Project

Documenting the craft of stacking things so they remain stacked. For a very long time.

Building local packages

This guide takes you through a basic package build without setting up the full dispatcher build system. It is ideal for simple packages that do not have complex dependencies, and for when you don't want to set up a full build system. All you need is the pkg tool that is already present on your system.

If you do want the whole thing, with gates, review and automatic rebuilds, then Deploying the build system is the guide you want instead. There's also a video of a local build if you'd rather watch one first.

Here are the steps, and you'll want to walk them in this order:

Before you start

pkg build runs the recipe's install phase inside a user namespace with several UID ranges. That only works if your user has a subordinate UID/GID range. In most cases you can just give your user the standard range of 65536 IDs, starting at 100000:

$ echo "$USER:100000:65536" | sudo tee -a /etc/subuid /etc/subgid
$ cat /etc/subuid
<user>:100000:65536
$ cat /etc/subgid
<user>:100000:65536

The build also needs network access, to fetch the source tarball and the buildroot packages from the repositories configured in /etc/pkg/repos.d/.

Write the recipe

A recipe is a directory containing a recipe.toml, plus optional files that this guide doesn't cover. The [recipe] block gives the name, version and release. Each [[source]] gives an upstream tarball and its SHA-256, which pkg build checks for you.

The build system doesn't need to be declared. pkg build detects it, which is autotools here, and runs configure, make and make install for you.

$ mkdir hello
$ $EDITOR hello/recipe.toml
# GNU hello -- a first local pkg build.

[recipe]
name = "hello"
version = "2.12.3"
release = 1
description = "GNU hello -- the canonical example of a GNU package"
homepage = "https://www.gnu.org/software/hello/"
license_hint = "GPL-3.0-or-later"
summary = """
GNU hello prints a friendly greeting. It exists to show how a GNU package
is laid out and built, which also makes it a good first recipe.
"""

[[source]]
name = "hello"
url = "https://ftp.gnu.org/gnu/hello/hello-2.12.3.tar.gz"
sha256 = "0d5f60154382fee10b114a1c34e785d8b1f492073ae2d3a6f7b147687b366aa0"
unpack_to = "."

Create a build key

Everything that pkg build publishes is signed with your personal Ed25519 build key. pkg build won't create one for you, so make it once:

$ pkg keygen
wrote build key ~/.config/pkg/build.key
  public key 1477cf8e47e283811c49692d0b7ab047d2ac053019eab10527a08143c1ba4998

Note the public key, and write it down somewhere. Any machine that installs your packages needs it to trust them, as you'll see when you install the bundle further on.

Build the package

$ pkg build ./hello --out out

Here's what pkg build does:

  • it downloads the source and checks its SHA-256,
  • it picks a build probe, autotools in this case,
  • it composes a clean, signature-verified buildroot from the repo chain. It never uses the host's /usr for this,
  • it runs configure, build and install in a sandbox,
  • it splits the installed files into subpackages,
  • and it signs those and publishes them into the local repository out/, which it creates on first use.

The build log goes to stderr, and it's about 2,200 lines of it here. These are the lines that matter:

autobuilder-core: materializing 1 source(s):
  + hello https://ftp.gnu.org/gnu/hello/hello-2.12.3.tar.gz sha256=0d5f6015...6aa0 -> .../build/. (stripped prefix hello-2.12.3/)
autobuilder-core: probe selected: autotools (applying: autotools)
build: building hello in sandbox
autobuilder-core: executing 1 build plan(s):
  + probe autotools:
      configure: ... '/build/configure' '--prefix=/usr' '--sysconfdir=/etc' ... '--host=x86_64-pc-linux-gnu'
      build: /usr/bin/make -j4
      install: /usr/bin/make DESTDIR=/build/staging/autotools -j4 install
=== phase: configure ===
=== phase: build ===
=== phase: install ===
autobuilder-core: classified into 52 subpackage(s): bin, data, debug, debugsrc, info, lang/ast, ... lang/zh_TW, man
build: warning [license-aggregate-divergence]: askalono detected "GPL-3.0-only" but recipe license_hint
is "GPL-3.0-or-later"; the hint is authoritative -- reconcile if the scan is right

That license warning is harmless. GNU hello is GPLv3-or-later, and its own --version output says "GPLv3+", so the recipe's license_hint is correct.

The summary goes to stdout, with the lang/* lines shortened here:

published 52 subpackage(s) into <workdir>/out
  hello/bin 2.12.3-1.x86_64
  hello/data 2.12.3-1.noarch
  hello/debug 2.12.3-1.x86_64
  hello/debugsrc 2.12.3-1.noarch
  hello/info 2.12.3-1.noarch
  hello/lang/ast 2.12.3-1.noarch
  ...                              (46 more lang/* translation subpackages)
  hello/lang/zh_TW 2.12.3-1.noarch
  hello/man 2.12.3-1.noarch
trust anchor <workdir>/out/metadata/1.root.json (key 1477cf8e47e283811c49692d0b7ab047d2ac053019eab10527a08143c1ba4998)

The whole thing takes about 2 to 3 minutes, and most of that is spent composing the buildroot. The result is an ordinary signed repository of 744 KB:

$ ls out
data  metadata  pkgs

Create an installable bundle

A .pkgtar bundle is a single, self-describing, signed file. You can copy it to another machine, even an air-gapped one, and install it there without configuring any repository at all.

$ pkg bundle create --from-repo-dir out --output hello-2.12.3-1.pkgtar
wrote bundle: <workdir>/hello-2.12.3-1.pkgtar

$ du -h hello-2.12.3-1.pkgtar
340K    hello-2.12.3-1.pkgtar

$ sha256sum hello-2.12.3-1.pkgtar
e9442071397a9eca3973a88528e1ed8758e30255b3f0f86973b808274bffe065  hello-2.12.3-1.pkgtar

The --from-repo-dir option temporarily puts the output repository on top of the repo chain, so you don't have to configure it as a repository first. Without a --closure the bundle contains everything the repository ships, so all 52 subpackages. On the inside it's a tar of content-addressed payloads plus signed TUF metadata, 107 entries in this case:

$ tar tf hello-2.12.3-1.pkgtar | head -3
data/04/a6/04a668fbcb24117321269007eb91df597be94b6accc1eab2b55dfdbd9741f333
data/06/f0/06f0d86c4008ccc7e4265cf3a41a0581ad58633f49995985a06cf576b8f9bd0d
data/0d/f3/0df36c4b119a1a8fc5eb35ab509561e341d0b4f51ed1a630547a99e90f1a2af1

Two options are worth knowing about here:

  • --closure hello carries only hello's runtime content, so bin and data.
  • --full-closure also carries hello's runtime dependencies, for a target that has none of the distribution installed. Do note that this pulls in everything that hello links against dynamically, including glibc/lib.

Install the bundle

The bundle is signed with your build key, which this system hasn't seen before. Installing it without trusting that key is refused:

$ pkg bundle install --dry-run hello-2.12.3-1.pkgtar
error: offline: hello/bin 2.12.3-1 is signed by 1477cf8e47e28381, which signs nothing installed here;
pass --trust-key 1477cf8e47e283811c49692d0b7ab047d2ac053019eab10527a08143c1ba4998 to accept it

So install it as root, and pass the public key from Create a build key:

$ sudo pkg bundle install \
    --trust-key 1477cf8e47e283811c49692d0b7ab047d2ac053019eab10527a08143c1ba4998 \
    hello-2.12.3-1.pkgtar
Plan:
  install:
    + hello  2.12.3-1  [/bin, /data]
  fetch: 2 package(s), 37.3 KiB to download
Transaction complete: 2 installed, 0 removed (#35, see `pkg history info 35`)

The bundle carries all 52 subpackages, but by default only the runtime content gets installed, which is hello/bin and hello/data. To install specific extras, use --spec, for example --spec hello/man --spec hello/lang/de. To install everything that's in the bundle, use --full-closure.

Verify the installation

First of all, the program works:

$ which hello
/usr/bin/hello

$ hello
Hello, world!

$ hello --greeting=Bonjour
Bonjour

$ hello --version
hello (GNU Hello) 2.12.3
Copyright (C) 2026 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by Karl Berry, Sami Kerola, Jim Meyering,
and Reuben Thomas.

The package manager knows about it:

$ pkg list | grep hello
hello                         2.12.3-1           -                  [/bin, /data]

$ pkg query hello
hello/bin 2.12.3-1 x86_64
hello/data 2.12.3-1 noarch

The files on disk match their signed payloads:

$ pkg verify hello
hello/bin
  ok    f /usr/bin/hello
hello/data
  ok    d /usr/share/pkg-licenses/hello
  ok    f /usr/share/pkg-licenses/hello/COPYING

And the install is a recorded transaction, just like any other:

$ pkg history info 35
#35  bundle-install  2026-10-08T20:11:04Z
command:  pkg bundle install --trust-key 1477cf8e...4998 hello-2.12.3-1.pkgtar
user:     root (sudo from <user>)
state:    9cf216310681 -> f93ee61c3ef5
changes:
  + hello/bin   2.12.3-1 (71b3578ae934)
  + hello/data  2.12.3-1 (7cd658813bff)

What you end up with

ArtifactWhat it is
hello/recipe.tomlThe recipe
~/.config/pkg/build.keyYour private build and signing key. Keep it safe
out/Signed local repository with 52 hello subpackages
out/metadata/1.root.jsonThe repository's trust anchor, for consumers of it
hello-2.12.3-1.pkgtarA 340 KB offline bundle, ready to install anywhere
/usr/bin/helloInstalled, working, verified, as transaction #35

Undoing it

$ sudo pkg remove hello

For more information about the pkg tool and all the extra options available for the sub commands used here, visit the Manual pages section.