The Ur Project

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

pkg-bundle(1)

NAME

pkg-bundle - produce or install an offline .pkgtar bundle

SYNOPSIS

pkg bundle create --output PATH [--from-repo-dir DIR] [--tuf-dir DIR] [--closure SPEC]... [--sign-key FILE] [--full-closure]

pkg bundle install bundle [--spec SPEC]... [--full-closure] [--trust-key HEX]... [--trust-key-file FILE]... [--no-verify] [--dry-run] [--force] [--keep-staged]

DESCRIPTION

Sometimes a repository URL just doesn't reach. The machine is on the other side of an air gap, or on a network that can't see the repository server, or you want to hand somebody your build on a USB stick. A .pkgtar bundle is for exactly that.

It's one file. Inside are the manifests of a set of packages, their payload blobs, and the TUF metadata that lists them. You make one with pkg bundle create, carry it over, and install it with pkg bundle install.

If what you want is to keep an air-gapped machine up to date over time, look at pkg-offline(1) instead. It builds on the same format, but it only carries what actually changed.

A bundle carries what you name

Nothing more. That's the default, and it's deliberate: the common case is handing over something you built. Expand a 40 KB package's closure and the distribution's libc comes along behind it, and the machine receiving it almost always has libc already.

--full-closure is for when it doesn't. It also resolves the runtime requirements of the packages you named, through the chain, and carries everything they reach. That's the bundle you want for an air-gapped machine being seeded from nothing, or a drop for a customer.

What gets bundled?

With --closure, the roots are the specs you give; see SPECS.

Without it, the roots are everything one repository currently ships: every package manifest its signed targets.json lists, every subpackage. That repository is the one behind --tuf-dir, or --from-repo-dir. This is the form for a pkg-build(1) output. You bundle the directory, not a list of names.

Each of those roots has to resolve through the chain to exactly the build targets.json lists. That means the pkg-meta whose digest targets.json signs, not just the same version. Two repositories quite often carry the same version built to different bytes. If the repository isn't in the chain, or a higher-priority repository carries the same name, the bundle is refused and the packages are named:

error: bundle: the chain does not resolve the packages /home/u/work/out/metadata lists to those builds -- configure that repo in repos.d, above the repos it builds on, or pass --from-repo-dir: which/bin 2.25-9 (the chain resolves another build: 2.25-9)

Which chain?

The configured one, from /etc/pkg/repos.d. When nothing is configured, it's the single local repository at --repo. Payloads are fetched the way pkg-install(1) fetches them: read from local repository directories, downloaded from remote ones.

--from-repo-dir puts one more local repository on top of that chain, for this command only. Typically it's a pkg-build(1) output. It behaves as if it had a drop-in with the highest priority, and its metadata/ doubles as the --tuf-dir. So bundling your own build needs no repos.d edit at all.

A bundle has to be complete to be useful. So a held set member, one that no repository in the chain pins, is fatal here, just like for pkg-compose(1):

error: bundle: set member(s) no repo in the chain pins: bzip2, tar

Why sign new metadata?

The bundle's TUF metadata is signed as the bundle is written, and it lists exactly the manifests the bundle carries. It isn't copied from a repository. A repository's own targets.json names that repository's current set, so a closure that spans a chain would travel with its dependencies unlisted. And when a bundle is unpacked into a repository, that list is what the repository reads as its current set.

That metadata says what the bundle carries. It never says whether to trust it: root.json does not travel in a bundle. The key is your pkg-keygen(1) build key, unless --sign-key names another.

Installing a bundle

pkg bundle install puts the bundle's packages on this system. It's recorded in the client state like any other install, so pkg-remove(1) and pkg-autoremove(1) see those packages afterwards.

The bundle is its own index and its own payload store, both held in memory. Nothing is written to a repository directory, and the machine doesn't need any repository configured to accept one.

Every manifest states its package name and its subpackage separately, so the names come straight out of the bundle. Each name is installed as its runtime content, exactly like pkg install <name> would, plus whatever [install] include_subpkgs adds. A bundle carrying hello/bin, hello/debug and forty-six hello/lang/\* installs hello/bin, and leaves the rest carried but not installed.

--spec narrows that down to the specs you name. --full-closure widens it to every manifest in the bundle.

Who vouches for it?

The signature on each manifest, and nothing else. It's checked before anything is planned, against the key that signs that package name on this system, or any --trust-key you pass. Since root.json never travels in a bundle, that signature is the only trust on this path. It's the same rule, and the same code, as pkg-offline(1); see TRUST there for the long version.

A bundle is not a repository

But it's close. The layout inside a .pkgtar is the repository layout, so if you want a persistent repository, untar it into a directory and you have one.

SPECS

Each --closure value uses the spec grammar of pkg(1), with one restriction:

name
The package's runtime content; see pkg-subpkgs(7).
name/subpkg
Exactly one subpackage. The name is split at the first /, so coreutils/lang/af is coreutils' subpackage lang/af.
+set
Every member of a set published across the chain. See pkg-sets(7).

The restriction: name=version is refused. A bundle is built from the chain's current set, so a pin could only repeat what's already current, or name something the current set doesn't carry.

OPTIONS

create

--output PATH
Required. The .pkgtar file to write. It's written beside that path and renamed into place, so a failed run never leaves half a bundle there.
--from-repo-dir DIR
A local repository to bundle from, put on top of the configured chain for this command only. Its metadata/ is the --tuf-dir unless you give one. Can't be combined with --repo-url, which replaces the configured chain instead of adding to it.
--tuf-dir DIR
A repository's metadata/ directory. Its targets.json supplies the roots when --closure is left out. Required unless --from-repo-dir or --closure is given. Nothing from it is embedded; the bundle's metadata is signed at bundle time.
--sign-key FILE
The Ed25519 private key, 32 raw bytes, to sign the bundle's TUF metadata with. That's the format pkg-sign(1) reads. Defaults to the pkg-keygen(1) build key.
--full-closure
Also carry everything the named packages need at runtime, resolved through the chain. Without it, the bundle is exactly what it names.
--closure SPEC
Can be repeated. The roots of the closure. Left out, it's every package the --tuf-dir repository currently ships.

install

Takes the bundle path, plus:

--spec SPEC
Can be repeated. Install only these, in the spec grammar of pkg(1) (name, name/subpkg), resolved against the bundle instead of the chain. By default, every name the bundle carries, as its runtime content.
--full-closure
Install every manifest in the bundle, debug symbols and translations included.
--trust-key HEX
Can be repeated. A key a bundled manifest may be signed with, on top of the keys that signed what this system already runs. You need it for a rotated key, a repository this system has never installed from, or the first bundle onto a bare machine.
--trust-key-file FILE
Can be repeated. A file with such keys, one per line. Blank lines and anything after a # are skipped, and a file with no key in it is refused.
--no-verify
Apply without checking manifest signatures at all. That throws away the only trust on this path, so it's for the lab, and it warns on stderr. It also sticks: the key each installed manifest names is trusted for later bundles, until that package is removed. Refused together with a trust key; say one or the other.
--dry-run
Print the plan, change nothing.
--force
Push past a dependency the bundle can't satisfy, and past a pre-flight GC blocker. Everything it overrides is reported, never silenced, and passing it makes the command ask for confirmation.
--keep-staged
Keep payloads that an earlier pkg stage-update staged, after they're installed.

All global options apply; see pkg(1). For create, a configured chain wins over --repo. So to bundle from a bare repository directory on a machine that has repositories configured, pass --repos-dir /nonexistent as well. install reads no repository at all: the bundle is the index and the store.

EXAMPLES

Bundling a set from a bare repository directory

Everything +console names, and nothing else:

pkg --repo /srv/repo/Common --repos-dir /nonexistent bundle create \ --output /mnt/usb/console.pkgtar \ --closure +console \ --tuf-dir /srv/repo/Common/metadata

Bundling a few packages

pkg --repo /srv/repo/Common --repos-dir /nonexistent bundle create \ --output /mnt/usb/vim.pkgtar \ --closure vim --closure vim/man \ --tuf-dir /srv/repo/Common/metadata

Bundling a local build

Everything a pkg-build(1) output repository ships, and nothing else:

pkg bundle create --from-repo-dir ~/work/out --output mywork.pkgtar

For a machine that has none of the distribution installed, carry the dependencies too:

pkg bundle create --from-repo-dir ~/work/out \ --output mywork-standalone.pkgtar --full-closure

The whole air-gap round trip

On the connected side. The isolated machine can't fetch what the set depends on, so this has to be a --full-closure bundle:

pkg bundle create \ --output /mnt/usb/console.pkgtar \ --closure +console \ --tuf-dir /srv/repo/Common/metadata \ --full-closure

On the isolated side, it's one step:

pkg bundle install /mnt/usb/console.pkgtar pkg list

Checking a bundle before trusting it

The plan says what would land, and changes nothing:

pkg bundle install /mnt/usb/console.pkgtar --dry-run

The bundle's layout is the repository layout, so you can read it without pkg at all, or unpack it and check it as a repository:

tar -tvf /mnt/usb/console.pkgtar | head mkdir /tmp/scratch-repo && tar -xf /mnt/usb/console.pkgtar -C /tmp/scratch-repo pkg verify-sig /tmp/scratch-repo/pkgs/bash/bin/5.3/4/x86_64/pkg-meta

Seeing what would go in

pkg -vvv bundle create --from-repo-dir ~/work/out \ --output /tmp/probe.pkgtar 2>&1 | grep 'bundle +'

Sizing a full-closure bundle before building it

A compose dry run resolves the same closure and writes nothing:

pkg --json compose root --output /tmp/x \ --closure +console --trust-key "$PUBKEY" --dry-run \ | jq '.install | length'

Seeding a fresh machine

A bare machine has no installed package to learn a trust key from, so the first bundle names one. Without it, the install refuses before planning anything:

pkg bundle install /mnt/usb/base.pkgtar --trust-key "$PUBKEY" pkg bundle install /mnt/usb/console.pkgtar

EXIT STATUS

0 on success. 1 on any failure: no signing key, a closure that doesn't resolve, a held set member, a version-pinned spec, a root the chain resolves to another build, a missing payload, a malformed bundle, or a write error.

SEE ALSO

pkg(1), pkg-build(1), pkg-offline(1), pkg-compose(1), pkg-install(1), pkg-repo(1), pkg-verify-sig(1), pkg-repository-layout(7), pkg-sets(7)

docs/components/pkg-tar.md, docs/design/repo.md