pkg-upgrade(1)
NAME
pkg-upgrade - converge the installed set onto what the chain pins
SYNOPSIS
pkg upgrade [--dry-run] [--provisional] [--keep-staged] [--force] [name...]
pkg upgrade [--dry-run] [--keep-staged] [--force] --to file
DESCRIPTION
Every repository in the chain publishes pins: for each package name, the one build that's current. pkg upgrade makes this system match them. That's called converging, and it's a different question from "is there something newer?". You'll see why below.
With no names, every installed package is converged. Every selection root is also expanded again, so anything the roots want that isn't installed gets installed; see SELECTION ROOTS.
With names, only those are converged, and they have to be installed already. A scoped upgrade never installs a fresh name. A bare name means the whole package, not one subpackage of it that pkg guessed at.
A +set name converges each installed member of the set, nested sets included. A member that isn't installed, or that no repository carries, is skipped with a note on stderr; pkg install +set is what adds it. A set none of whose members is installed is an error.
With --to, the target is one published build instead of the chain's pins; see CONVERGING ONTO A BUILD.
update is an undocumented alias, and it does exactly the same thing.
Why "converge" and not "upgrade"?
Because a pin is a named pointer, not a version floor. Convergence asks one thing: is the installed artifact the one the pin names? It compares payload digests to find out. It never asks whether something is newer.
That gets you three things a version comparison can't express:
- Moving a package to another repository in the chain is real work, even at the same version-release, because the payloads differ. Two repositories can publish the same version-release built against different buildroots.
- A release bump that rebuilt to identical bytes isn't work at all, and is left alone.
- A downgrade needs no special case. The pin points where it points.
Every installed package is converged, even one that's already at its pin's digest. That emits no work for the package itself, but it still walks its requirements. And that walk is the only thing that pulls in a dependency the package gained, one an earlier install left behind, or a new provider for a subpackage that upstream split out.
SELECTION ROOTS
The roots that pkg-install(1) records are intent, not a frozen list. A bare pkg upgrade expands them again against the current chain, exactly the way pkg install would:
- A set root expands to its current members. A member the producer added to the set gets installed.
- A bare name, whether a package root or a bare set member, expands to its runtime subpackages at the winning pin, plus whatever [install] include_subpkgs in pkg.toml(5) selects. A runtime subpackage the package gained since you installed it gets installed, and so does one a newly added selector covers.
- A name
/subpkg coordinate expands to that one subpackage.
Anything that expansion names that isn't installed is installed at its pin. This side only adds. Nothing is removed because a root stopped wanting it; that's pkg-autoremove(1).
A root the chain can't answer (a set no repository defines, a name no repository pins, a coordinate whose pinned build no longer has that subpackage) is held and reported, and the rest of the upgrade goes ahead:
held (selection roots the chain cannot answer): +ghostset, foo/bin
Why did the subpackage I removed come back?
Because a bare root still asks for it. Removing a subpackage by hand doesn't change the root. Record the coordinates you want instead of the bare name, or take the selector out of pkg.toml(5). A system with no roots recorded installs nothing new.
REMOVED, HELD, OR LEFT ALONE
Things can go missing from the chain in different ways, and they're handled differently. The difference matters.
- A subpackage the pin no longer carries is removed.
- Its files belong to a version-release that's no longer current. This only happens when the winning repository demonstrably carries the pinned build without it.
- A name no repository in the chain pins is held.
No pin means no answer, and guessing one is how a system silently loses a package that just moved between repositories. Held names are reported and left alone:
held (no repo in the chain pins these): foo/bin, foo/lib
A repository that can't answer for the name at all is a reason to hold. Never a reason to remove.
- A name a local pin holds stays at its pin.
Pins in pkg-pins.d(5) sit above every repository. The upgrade leaves the name where it is and tells you, with both builds and the pins file:
openssl held by pin at 3.5.1-2 (Base has 3.5.2-1) -- /etc/pkg/pins.d/rollback-50.toml
--forcedoesn't get past a pin. Release it withpkg unpinfirst; see pkg-pin(1).
CONVERGING ONTO A BUILD
Sometimes you don't want whatever the chain pins right now. You want the exact build somebody told you to test. pkg upgrade --to file converges onto the entries of one build, and that's how a host that updates in place lands on it.
file is either a pins file (name.pins.toml, as a pins composition publishes it and pkg offline export-pins writes it) or a published build's image.json, whose closure lists the same entries. Which one it is gets decided by content, not by file name.
In one transaction, every name/subpkg the file names ends up installed at exactly the payload digest it names. That can mean upgraded, downgraded, moved to another build at the same version-release, or installed fresh. An entry that's installed fresh is recorded as a name/subpkg selection root, so pkg-autoremove(1) doesn't take back what the build put there.
What about packages the file doesn't name?
They're left alone, and that's enforced:
- A plan that would upgrade or remove one is refused, naming it, with or without
--force. A build that needs it changed has to name it. - A plan that would leave one with a requirement nothing satisfies is refused, naming it and the requirement. With
--forcethe plan goes ahead and reports that requirement with everything else force overrode. A requirement that was already unsatisfied before the plan doesn't count against it.
Where does the trust come from?
Not from the file. The digests only select. Every manifest the transaction uses comes from the configured repository chain and is verified exactly as it would be for a plain upgrade. The file's repos and trust_keys aren't consulted.
Entries are looked up in the chain's current sets first, and only the ones those don't serve are looked up in the superseded history of their names. An entry no configured repository serves at all is refused, naming each one, rather than quietly converging onto some other build:
error: resolve: 1 entr(ies) name a digest no configured repo serves, current or superseded; refusing rather than converging onto another build: NetworkManager/bin 1.51.4-1 ffff...ffff
After the transaction commits, the installed state is checked against every entry. So a successful run means pkg offline export-pins carries the file's digest for every entry the file names.
OPTIONS
--dry-run- Compute and print the plan, change nothing.
--provisional- Converge onto the staged pins (metadata/provisional_pins.json) instead of the blessed ones (metadata/pins.json). This is how you test a staged promotion before it's blessed. Only a repository read from a local directory can have both files; a remote repository serves the staged pins either way. See pkg-chain(7).
--tofile- Converge onto the build file describes; see CONVERGING ONTO A BUILD. Can't be combined with package names or
--provisional. --keep-staged- Keep staged payloads after they're installed. By default a successful upgrade drops every staged payload whose bytes are now installed, whether pkg-stage-update(1) or this upgrade's own prefetch put it there. Once a payload is unpacked into the system, its staged copy is the same bytes stored twice. Keep them to apply the same upgrade again offline, or when the cache is deliberately shared.
--force- I know, do it anyway. Proceeds past a dependency the chain can't satisfy (that edge is dropped and the rest of the closure installs) and past a pre-flight GC blocker on a removal: a running process maps an outgoing file, an installed binary still needs a soname, a kernel module is loaded. It won't install a package that doesn't exist, and it won't break a pin. Everything it overrides is reported, never silenced, and passing it makes the command ask for confirmation.
All global options apply; see pkg(1).
CONFIRMATION
At the default confirm level, destructive, an upgrade does not prompt. Removals that come with an upgrade (a shed subpackage, a replaced provider) aren't treated as destructive. --force does prompt. Set confirm = all in pkg.toml(5) if you want to be asked every time.
OUTPUT
The plan uses the shape described in pkg-install(1). Rebuilds in place and repository moves show their digests:
Plan: remove:
- util-linux 2.42.2-7 [/lib]
upgrade: ~ openssl 3.5.4-2 -> 3.5.5-1 [/bin, /data, /lib] ~ expect 5.45.4-6 -> 5.45.4-6 [/bin] (rebuild a1b2c3d4e5f6 -> 0f9e8d7c6b5a)
A bare upgrade converges every installed subpackage, so an uncollapsed plan repeats each name once for every class it was split into. That's what --collapse=none prints. The default folds it the way pkg-list(1) folds a listing. See pkg(1) and pkg-subpkgs(7).
Payloads are pulled four at a time into the staged cache before anything is extracted, so one big payload doesn't hold up the rest. After a committed transaction, the staged copies of what's now installed are dropped, and a line says how much that freed:
reclaimed 9 staged package(s), freed 152.4 MiB (--keep-staged to retain)
Watch for the needs restart block after the transaction. A process still mapping the replaced files keeps running the old code, and this is the only notice you get.
With --to, a line before the plan names the file, its format, how many entries came out of superseded history, and how many installed packages the file leaves alone. After a committed transaction, another line confirms every entry is at its digest.
Under --json, the plan document gains orphaned (held names), held_roots and held_by_pin. With --to it also gains target (file, format, entries, composition, build_id), from_history and untouched, and the result document gains target, converged and roots_added. See pkg-cli/JSON.md.
EXAMPLES
The everyday loop
pkg upgrade --dry-run pkg upgrade
Upgrading one package
pkg upgrade openssl pkg upgrade openssl curl
Upgrading one subpackage
pkg upgrade openssl/lib
Testing a staged promotion before it's blessed
pkg upgrade --provisional --dry-run pkg upgrade --provisional
Converging onto a published build
curl -O "$build/baremetal.pins.toml" pkg upgrade --to baremetal.pins.toml --dry-run pkg --yes upgrade --to baremetal.pins.toml pkg offline export-pins
The build's image.json works the same way, and its build_id is echoed in the plan:
pkg --json upgrade --to image.json --dry-run | jq .target
Why is this package held?
pkg upgrade --dry-run pkg repo list --all pkg set show +console
A held name usually means a repository is disabled that shouldn't be. If the package really did leave the distribution, remove it yourself:
pkg remove foo
Why is it upgrading to the same version?
pkg upgrade --dry-run | grep rebuild pkg info expect/bin
The two digests differ, so the artifacts differ, so it's real work. That's normal after a package moves from one repository to another in the chain.
Non-interactive
pkg --yes upgrade pkg --json upgrade --dry-run | jq '.upgrade | length'
How much will it fetch?
pkg --json upgrade --dry-run | jq '.estimated_disk_bytes' pkg --json upgrade --dry-run | jq '.fetches | length'
Watching the resolve
pkg -v upgrade --dry-run pkg -vvv upgrade --dry-run 2>&1 | grep 'closure ~'
Converging against a repository that isn't configured
pkg --repo-url http://staging:8082/Common \ --tuf-root /etc/pkg/common.root.json \ upgrade --dry-run
Which processes need a restart afterwards?
pkg --json --yes upgrade | jq -r '.needs_restart[] | "(.pid) (.command)"'
Every pid listed is a real process. But when the scan itself couldn't run, needs_restart is empty and restart_scan_unavailable carries the reason. Check it before you trust an empty list.
EXIT STATUS
0 on success, including a plan with nothing to do. 1 on any failure or a declined confirmation. Held names aren't a failure.
With --to, every refusal is a failure, and nothing is applied: an entry no repository serves, a package the file doesn't name that the plan would change or break, a file that's neither format.
SEE ALSO
pkg(1), pkg-install(1), pkg-remove(1), pkg-autoremove(1), pkg-check-update(1), pkg-stage-update(1), pkg-offline(1), pkg-pin(1), pkg-repo(1), pkg-triggers(1), pkg.toml(5), pkg-pins.d(5), pkg-repos.d(5), pkg-chain(7), pkg-subpkgs(7)
docs/design/repo.md, docs/design/downgrades.md, docs/design/staged-promotion.md, docs/design/abi-rebuild.md