A small, read-only, repository-aware companion to apt list.
apt-lists answers one question that apt list cannot:
Which installed package versions are currently provided by a specific APT repository?
$ apt-lists --installed --repo https://deb.debian.org/debian-security/
PACKAGE VERSION ARCH REPOSITORY
libbaz 3.1-2+deb13u1 amd64 https://deb.debian.org/debian-security/
seconly 2.0 all https://deb.debian.org/debian-security/It is not a repository management tool. It never downloads package
lists, installs or removes packages, and never modifies APT configuration or
the dpkg database. It only reads the state that libapt-pkg already has.
A full annotated transcript is available in
docs/example-output.md.
apt-lists determines that:
the currently installed version of a package is available from repository X according to the current local APT cache.
It does not determine — and APT/dpkg cannot determine — that:
the package was originally installed from repository X.
APT and dpkg do not record the installation source in the package status
database. If foo 2.0 is currently in the cache from the archive and
foo 1.0 from an older pocket, a system with foo 2.0 installed will report
the archive — even if the package originally came from somewhere else and was
later upgraded. Treat all output as a statement about current cache state,
not history.
A package is reported for --repo only when the exact triple
installed package name + installed version + installed architecture
is provided by that repository, per libapt-pkg's own VersionFile records.
The tool never merely checks whether a repository provides a package name:
archive: foo 2.0-1 (candidate)
debian-updates: foo 2.0-1+deb13u1 (candidate — newer)
Installed: foo 2.0-1
$ apt-lists --installed --repo https://deb.debian.org/debian-updates/
-> foo is NOT reported (the exact installed version is not in the pocket)
$ apt-lists --installed --repo https://deb.debian.org/debian/
-> foo IS reported
Architecture is part of the identity: a version with Architecture: all is
reported as all; foo:amd64 and foo:i386 are independent.
A version provided by several repositories is reported with all of them
(no candidate-priority filtering). A version that no repository in the cache
provides (e.g. a locally installed .deb) is shown with an empty repository
list in --installed, and never matches a --repo filter.
Suite, Codename, Component, Origin and Label are not globally unique.
apt-lists therefore builds identity from what libapt-pkg itself keeps for
each package index (pkgCache::PackageFile + its linked
pkgCache::ReleaseFile, exposed by oma-apt as PackageFile):
| field | source | example |
|---|---|---|
uri |
IndexFile::ArchiveURI("") (REPO_URI) |
https://deb.debian.org/debian-security/ |
site |
PackageFile::site() (hostname) |
deb.debian.org |
filename |
PackageFile::filename() (local index) |
.../lists/deb.debian.org_debian_..._Packages |
archive |
PackageFile::archive() (suite) |
trixie-security |
codename |
PackageFile::codename() |
trixie |
origin |
PackageFile::origin() |
Debian Security — not related to the host |
label |
PackageFile::label() |
Debian Security |
component |
PackageFile::component() |
main |
arch |
PackageFile::arch() (index architecture) |
amd64 |
Repositories are grouped by their base URI (one source entry may serve several suites, components and architectures — each combination is a separate package index inside the same repository). Note that Origin and repository hostname are distinct fields and are preserved as such.
libapt-pkg does not expose a lossless way to reconstruct the exact URI
spelling from the list file name (it is a quoted, lossy encoding), so the
values above are reported raw. The URI is the one libapt itself reports
and carries a trailing slash; matching normalizes trailing slashes and the
case of scheme/host.
- full URI:
--repo https://deb.debian.org/debian-security/— exact URI match - hostname:
--repo security.debian.org— matches the site - host + path:
--repo deb.debian.org/debian-security
If a hostname selector matches several distinct repository URIs, the tool
fails with an ambiguity error listing the candidates instead of silently
choosing one. This is not an edge case on a real Debian system:
deb.debian.org hosts /debian, /debian-security and /debian-updates,
so a bare --repo deb.debian.org is refused and a path is required. Three
outcomes are distinguishable:
- repository matched, packages matched → results
- repository matched, no installed packages match → empty output, exit 0
- selector matched no repository → explicit error, exit 1
apt-lists --installed # all installed packages + providers
apt-lists --installed --repo <REPOSITORY> # installed from one repository
apt-lists --repos # repositories known to the cache (+ package counts)
apt-lists <package> # all versions of a package + repositories
apt-lists <package>:<arch> # e.g. apt-lists foo:i386
apt-lists --all # all available package versions (default)
apt-lists --manual-installed # like `apt list --manual-installed`Short forms: -i (--installed), -r (--repo), -R (--repos),
-a (--all), -m (--manual-installed), -j (--json).
Add --json for machine-readable output. --repo composes with
--installed, --all and package queries.
Table cells are single whitespace-free tokens: multi-value cells (components,
archs, repository URIs) are joined by a comma without a space, so the
alignment padding is the only whitespace and sort -k<n> / awk address
columns directly. --no-headers drops the header row (the column widths are
then computed from the data alone):
$ apt-lists --repos --no-headers | sort -k5,5n
https://deb.debian.org/debian-security/ trixie-security main amd64 2
https://deb.debian.org/debian-updates/ trixie-updates main amd64 2
https://ftp.us.debian.org/debian/ trixie main amd64 3
https://deb.debian.org/debian/ trixie main amd64,i386 5The one exception is the row-final [installed] marker of package queries,
which follows the REPOSITORY cell after two spaces (an extra token, so
-k1..-k4 still address the columns).
apt-lists can print completion scripts for your shell:
apt-lists --generate-completion bash | sudo tee /usr/share/bash-completion/completions/apt-lists
apt-lists --generate-completion zsh > "${fpath[1]}/_apt-lists"
apt-lists --generate-completion fish > ~/.config/fish/completions/apt-lists.fishSupported shells: bash, zsh, fish, elvish, powershell.
Every package-listing mode uses the same two shapes, so scripts can rely on a uniform field set:
- without
--repo:{ "packages": [ ... ] }, every row carries itsrepositoriesarray and theinstalledflag; - with
--repo:{ "repository": {...}, "packages": [ ... ] }, the selected repository is reported once at the top level, so the rows omit their (redundant)repositoriesarray.
$ apt-lists --installed --repo https://deb.debian.org/debian-security/ --json{
"repository": {
"uri": "https://deb.debian.org/debian-security/",
"site": "deb.debian.org",
"origin": "Debian Security",
"label": "Debian Security",
"suites": [
{
"archive": "trixie-security",
"codename": "trixie",
"components": ["main"],
"architectures": ["amd64"]
}
]
},
"packages": [
{
"name": "libbaz",
"version": "3.1-2+deb13u1",
"architecture": "amd64",
"installed": true
},
{
"name": "seconly",
"version": "2.0",
"architecture": "all",
"installed": true
}
]
}Single-package queries (apt-lists foo --json) use the same packages
envelope as every other mode. With --repos --json, the per-suite detail
(suites with codename, components, architectures and the number of distinct
package names as packages) and the Release metadata (origin, label,
per-index index_filename/index_type) are reported in full. No canonical
URI reconstruction is promised: the values are the raw ones libapt-pkg
exposes.
- Debian (or derivative) with
dpkg; not portable to non-dpkg distributions. libapt-pkg≥ 6.0 (Debian 12+). Primary target: Debian 13 / APT 3.x /libapt-pkg.so.7.0(developed and tested against apt 3.0.3).- Build: Rust 1.85+ and
libapt-pkg-dev(for the headers). oma-apt0.13 is the APT interface — there is no second package-index parser in this codebase.
sudo apt install build-essential libapt-pkg-dev
cargo build --release
# binary: target/release/apt-lists- One
libapt-pkgcache is opened per invocation (viaoma-apt), packages are iterated once, and repository filtering happens in memory. Noapt-cachesubprocesses are spawned, and/var/lib/apt/listsis never parsed by hand. - The
Package → installed Version → VersionFile → PackageFilerelation is taken directly from libapt-pkg, which is what makes exact name+version+architecture matching possible without re-implementing any index logic. - The dpkg status database also appears as a
PackageFile(index typeDebian dpkg status file, never downloadable); it is excluded from repository provenance but is what makes locally installed packages visible. - The tool is strictly read-only:
Cache::update()is never called, nothing is marked for install/remove, no locks are taken. - Human tables always use the same columns: a
--repofilter narrows the rows, never the columns, and--reposprints one row per repository and suite so the table stays narrow. Cells are single whitespace-free tokens (multi-values joined by,), so--no-headersoutput pipes straight intosort -k<n>/awk. - Output is written through explicit, error-checked writes: when the reader
of a pipe goes away (
apt-lists -i | head), the tool exits quietly with status 141 (128 + SIGPIPE) instead of panicking.
The repository enforces a strict style/safety gate locally and in CI
(just check):
cargo fmt --check— no formatting differences allowed.cargo clippy --all-targets -- -D warnings— the lint configuration lives in the[lints]tables ofCargo.toml:clippy::all,clippy::pedanticandclippy::cargoare enabled, plusmissing_docs.unsafe_codeis denied: this crate contains nounsafeblocks. All libapt interaction goes through the safeoma-aptwrappers. The single sanctioned exception is oneunsafe { env::set_var }call in the test fixture (settingAPT_CONFIGbefore libapt initializes; commented in place).- CI (
.github/workflows/ci.yaml, GitHub Actions-compatible syntax for Forgejo/Gitea Actions) runs fmt, clippy and the test suite on every push and pull request.
The test suite does not touch the host system's APT state. Each test builds
an isolated APT root (own sources.list, package lists, dpkg status, auto
install marks and cache directories) and points libapt-pkg at it via the
documented APT_CONFIG environment variable. Covered scenarios include:
same suite on two official repositories (archive + mirror) staying distinct,
exact version matching (positive and negative), multiple providers of one
exact version, architectures (amd64/i386/all), candidate ≠ installed,
manual vs auto installed, status-only packages, config-files state, unknown
and ambiguous repository selectors, shell completion generation, uniform
table columns across modes, the compact --repos table, the JSON output
shapes, and broken-pipe handling (apt-lists -i | head exits quietly with
status 141) — both at the library level and end-to-end through the CLI
binary.
just test # or: cargo testGPL-3.0-or-later, see LICENSE. The oma-apt dependency uses the
same license.