The Ur Project

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

pkg-check-update(1)

NAME

pkg-check-update - report whether updates are available, changing nothing

SYNOPSIS

pkg check-update [--exact] [--provisional] [--no-cache] [name...]

pkg check-updates [--exact] [--provisional] [--no-cache] [name...]

DESCRIPTION

Is there anything to upgrade? That's the one question this command answers, and it changes nothing while doing so. No manifest is installed, no payload is fetched, no state is written.

Why not just use upgrade --dry-run?

You could. pkg upgrade --dry-run already tells you what would change. But it pays for a full index build to get there: every repository's manifests, one request per manifest, tens of megabytes against a chain the size of Base plus Common. That's fine when you're about to upgrade anyway. It's way too much for a question asked on a timer, where the answer is usually "no".

So by default this command diffs pin maps instead. A pin map is the list a repository publishes of which build is current for each name, and it's a few kilobytes. That gets compared against what's installed. And if a repository's timestamp.json still names the snapshot the metadata cache saw last time, it stops right there. On the common nothing-changed path, one small signed request per repository is the entire cost.

It opens the state read-only and needs no privileges. That's the whole point: a desktop applet or an operator's timer can run it.

Without names, every installed package is considered. Naming packages limits the check to them, and a name that isn't installed is an error, not an empty answer.

Four answers, not a count

Each installed name lands in one of these:

available
The chain pins a different N-V-R than the one installed.
held
No repository in the chain pins the name at all. It's reported, never dropped, and never guessed at. A package that just moved out of the chain must not look the same as one that's current.
held by pin
A local pin (pkg-pins.d(5)) holds the name at a build the chain has moved past. It's listed with both builds and the pins file, and isn't counted as available: applying it means releasing the pin, not running an upgrade.
current
The installed N-V-R equals the pin. Not printed.

Can I trust the cheap answer?

Mostly. But it's advice, and you should know where it's blind.

Convergence is about payload-digest identity, not about comparing N-V-Rs; see pkg-upgrade(1). A pin diff only compares N-V-Rs. So it can't see a rebuild at the same N-V-R whose bytes changed, a subpackage the new build sheds, or a dependency a package gained.

--exact resolves the plan pkg-upgrade(1) would apply, and that's the authoritative answer. The two tiers measure different things, not the same thing with different accuracy. That's why the output names the tier that produced it.

A repository that published no pin file can't be answered for cheaply. Rather than report its names as held, which reads as "orphaned" when it really means "not asked", it's named on stderr:

warning: Common published no pin file; names only it answers for are reported as held. Use --exact for an authoritative answer.

OPTIONS

--exact
Resolve the full plan instead of diffing pins. It's authoritative: it sees dependency closures, shed subpackages and payload digests. But it costs a complete index build, which is exactly what the default exists to avoid. On this tier the held list is always empty, and a dependency the plan would newly install shows up with - as its installed side.
--provisional
Compare against the staged provisional pins (metadata/provisional_pins.json) instead of the blessed ones (metadata/pins.json), on either tier. 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).
--no-cache
Ignore the cached metadata and fetch every repository's chain again. A cache hit is already signature-verified, so this is for debugging the cache itself, not for trusting the answer more. It only affects the pin tier.

All global options apply; see pkg(1).

OUTPUT

One line per available update, installed state on the left:

openssl 3.5.6-13 -> 3.5.6-14 [Common] vim 9.1.1848-2 -> 9.1.1902-1 [Common]

The pin tier names the repository, because it knows which one answered. --exact resolves through the whole chain and doesn't attribute any single one, so it prints just the arrow:

openssl 3.5.6-13 -> 3.5.6-14

Held packages follow, after a blank line:

2 package(s) held -- no repo in the chain pins them: oldpkg 1.2-3 gonepkg 4.5-6

Then names a local pin holds:

1 package(s) held by a local pin (pkg unpin to release): curl held by pin at 8.16.0-1 (Base has 8.17.0-1) -- /etc/pkg/pins.d/local.toml

When nothing is available, the output ends with:

no updates available

JSON

{ "ok": true, "tier": "pins", "updates_available": true, "available": [ { "name": "openssl", "installed": "3.5.6-13", "available": "3.5.6-14", "repo": "Common" } ], "held": [ { "name": "oldpkg", "installed": "1.2-3", "available": null, "repo": null } ], "held_by_pin": [ { "name": "curl", "pinned": "8.16.0-1", "chain": "8.17.0-1", "repo": "Base", "file": "/etc/pkg/pins.d/local.toml" } ] }

tier is pins or exact. repo is null on the exact tier, and available is null for a held row. held_by_pin lists pinned names the chain has moved past; chain and repo are null when no repository pins the name anymore.

EXAMPLES

Is there anything to do?

pkg check-update

Asking about one package

pkg check-update openssl pkg check-update openssl kernel vim

The authoritative answer

pkg check-update --exact

A timer that only speaks up when there's something to say

pkg check-update >/tmp/updates || cat /tmp/updates

The || fires on any non-zero exit: 100 when updates exist, and 1 when the check itself failed. Both are worth hearing about.

A monitoring check

pkg check-update >/dev/null case $? in

  1. echo "OK: up to date" ;;
  2. echo "WARNING: updates available" ;;

*) echo "UNKNOWN: could not tell" ;; esac

Scripting

pkg --json check-update | jq -r '.available[].name' pkg --json check-update | jq -e '.updates_available' >/dev/null pkg --json check-update | jq -r '.held[] | "(.name) (.installed)"'

The everyday loop

pkg check-update && exit 0 pkg stage-update pkg upgrade

Testing a staged promotion

pkg check-update --exact --provisional

Debugging the metadata cache

pkg check-update --no-cache pkg -v check-update --no-cache

Where does the time go?

pkg -v check-update pkg -v check-update --exact

The -v line names the tier and the counts:

check-update [pins]: 2 available, 0 held

EXIT STATUS

This is the one pkg command that uses its exit code to carry the answer. It follows the dnf convention, so an existing timer or monitoring check works unchanged:

0
No updates available.
100
Updates are available.
1
Couldn't tell: unreadable state, an unreachable repository, a named package that isn't installed.

Held packages alone don't make it 100, and neither do names held by a pin. A held package isn't an update.

SEE ALSO

pkg(1), pkg-stage-update(1), pkg-upgrade(1), pkg-list(1), pkg-pin(1), pkg-pins.d(5), pkg-state(5), pkg-chain(7), pkg-repository-layout(7)

docs/design/repo.md, docs/design/staged-promotion.md, pkg-cli/JSON.md, pkg-cli/VERBOSITY.md