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
/, socoreutils/lang/afis coreutils' subpackagelang/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
--outputPATH- 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-dirDIR- A local repository to bundle from, put on top of the configured chain for this command only. Its metadata/ is the
--tuf-dirunless you give one. Can't be combined with--repo-url, which replaces the configured chain instead of adding to it. --tuf-dirDIR- A repository's metadata/ directory. Its targets.json supplies the roots when
--closureis left out. Required unless--from-repo-diror--closureis given. Nothing from it is embedded; the bundle's metadata is signed at bundle time. --sign-keyFILE- 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.
--closureSPEC- Can be repeated. The roots of the closure. Left out, it's every package the
--tuf-dirrepository currently ships.
install
Takes the bundle path, plus:
--specSPEC- 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-keyHEX- 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-fileFILE- 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-updatestaged, 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