The Ur Project

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

pkg-license(1)

NAME

pkg-license - report what is installed under which license, and print the license text

SYNOPSIS

pkg license [options]

pkg license [options] name[/subpkg]

DESCRIPTION

Sooner or later somebody asks what your machine is licensed under. Maybe it's legal, maybe it's a customer, maybe it's you. There are really two questions there: what is every installed package licensed under, and what does that license actually say. The package manager is the only thing on the system that can answer both honestly, so it does.

Both answers are local. Every installed manifest carries a [license] table with two things in it. spdx is the license expression the recipe declared. files is the list of license files the build found, installed under /usr/share/pkg-licenses/<pkg>/. Nothing is fetched, and nothing is rescanned.

With no name, you get the whole installed set, one row per package, collapsed per --collapse. With a name, you get that one package in detail.

Declared and detected

The two are shown side by side, and never reconciled. declared is the package-level expression, carried verbatim from the recipe's license_hint, and it's the authoritative one. text is evidence, per file: every file the license scan matched, with the SPDX id it matched for that file alone.

They can disagree, and usually that's not a bug. A vendored source file under a compatible license that the package-level expression doesn't name is completely ordinary. This command's job is to put that in front of a human, not to rule on it.

What can't be identified?

Two states get called out instead of passed over. An unknown that looks like an answer is worse than an unknown that says so.

unidentified
The expression uses the SPDX LicenseRef- (or DocumentRef-) escape hatch. That's part of the SPDX standard, but it names a license no SPDX id resolves.
no text
[license] files is empty. The license scan matched nothing in the source tree, so no license text was installed, and the declared expression stands on the recipe's hint alone.

The two are independent. A package can be either, both or neither, and --unknown is the standing report of everything in either state.

Installed, or in the repository?

A name is looked up in the installed set first, and the repository chain second. The report tells you which one answered: (installed) or (repo). "What am I running" and "what would I be agreeing to" are both questions about one name. --all forces the second one.

OPTIONS

--text

Print the license text, each file under a ==> path <== header, in the order [license] files lists them. Needs a name, and the package has to be installed, because it reads the installed files.

It exits non-zero when there's nothing to print, instead of succeeding silently. If you asked for the license text, an empty answer must never look like a permissive license. A file that can't be read gets a warning on stderr, and the others still print.

--files
List the license files as spdx path, one per line. That's the per-file evidence, without the block around it. Needs a name.
--unknown
Report only the packages that are unidentified or ship no text, each with the reason, followed by a count of how many rows that is. Exits zero: it's a report, not a check.
--summary

Count packages per license id instead of listing them, most common first, with totals underneath.

A count is followed by (N optional) when N of those packages only name the id on one branch of an OR. That's a license you're offered, not one you're bound by. Without that split, a dual-licensed set reads as far more encumbered than it really is, and avoiding that misreading is the point of this report.

--match=GLOB

Report only packages whose expression names a matching license id or WITH exception. Case-insensitive, because SPDX ids are, and nobody should have to reproduce their canonical spelling. * and ? glob (GPL-*). Each row then says which term matched, and whether the package is actually bound by it.

It's a filter, not a report, and it applies before everything else. So it narrows the listing, the summary and the policy check alike.

--mismatch

Report packages that ship a license file whose detected id the declared expression doesn't name, with the file each one came from.

It's a review aid, not a list of defects. A vendored source file under a compatible license is ordinary. The grant suffix isn't a mismatch either: declaring GPL-2.0-or-later while shipping the text of GPL-2.0-only is how a GPL package is normally packaged. Comparing those verbatim reported two thirds of a real installed set as disagreeing with itself.

--check

Check the reported set against [license] in pkg.toml(5), and exit 1 if anything violates it, naming the refused term on each row.

This is for what's already installed. A policy written on Tuesday has nothing to say about what arrived on Monday, until something asks. It never prompts and never changes anything.

With no [license] table configured, it says so and exits 0. That's not a clean bill of health, and it doesn't pretend to be one.

--all
Report the repository chain instead of the installed set. With a name, answer from the chain instead of preferring the installed copy.

--summary, --unknown, --mismatch and --check are separate reports: ask for one at a time. They, and --match, work over a set, so they take no name.

All global options apply; see pkg(1). --collapse accepts main (the default) and none. sets and roots explain an installed set from its selection roots, which only pkg-list(1) can do.

OUTPUT

The listing

acl GPL-2.0-or-later AND LGPL-2.1-or-later [/bin, /lib] acl/doc GPL-2.0-or-later AND LGPL-2.1-or-later fontconfig MIT [/lib] (no text) tzdata LicenseRef-public-domain [/data] (unidentified, no text)

The bracket lists the runtime subpackages the row collapsed, same as pkg-list(1). The flag in parentheses at the end is never a second license. No flag means the expression is an SPDX id and the text is on disk.

One package

acl 2.4.0-6 (installed) subpkgs: /bin, /doc, /lib, /man declared: GPL-2.0-or-later AND LGPL-2.1-or-later text: /usr/share/pkg-licenses/acl/doc/COPYING GPL-2.0-only /usr/share/pkg-licenses/acl/doc/COPYING.LGPL LGPL-2.1-only

You get one block per distinct license fact, not one per subpackage. [license] is the same table across a whole source build, so printing it for each subpackage would just repeat itself. Two blocks under one name therefore mean something real: a half-upgraded package, or subpackages that genuinely differ. --collapse=none asks for a block per subpackage anyway.

A summary

GPL-2.0-or-later 118 (11 optional) GPL-3.0-or-later 97 (2 optional) LGPL-2.1-or-later 91 (2 optional) X11 32 -- 500 packages, 45 distinct ids, 13 unidentified, 76 without text

A policy check

nmap LicenseRef-NPSL-0.95 [/bin, /data] (unidentified, and unknown = "deny") p7zip LGPL-2.1-or-later AND LicenseRef-unRAR [/bin] (denied: LicenseRef-unRAR) -- 2 of 500 packages violate [license]

EXAMPLES

What is this machine licensed under?

pkg license

What can't be identified?

pkg license --unknown

What is it mostly licensed under?

pkg license --summary

Is there any AGPL here, and am I bound by it?

pkg license --match 'AGPL-*'

Does anything installed violate my policy?

pkg license --check

Where does a shipped license file disagree with its package?

pkg license --mismatch

What one package says, and what it says in full

pkg license openssl pkg license openssl --text pkg license openssl --text | less

What would a package I haven't installed commit me to?

pkg license nmap --all

Every distinct license expression on the machine, by frequency

pkg --json license \ | jq -r '.packages[].declared' | sort | uniq -c | sort -rn

Anything mentioning the AGPL

pkg --json license \ | jq -r '.packages[] | select(.declared | test("AGPL")) | "(.name)/(.subpkg)"'

Everything that ships no license text

pkg --json license \ | jq -r '.packages[] | select(.text == []) | .name' | sort -u

Every license file on the system, with the id matched for it

pkg --json license \ | jq -r '.packages[].text[] | "(.spdx)\t(.path)"' | sort -u

EXIT STATUS

0
The question was answered. That includes an answer like "six packages can't be identified".
1

--check found a violation, --text had no license text to print, an argument was unusable, or the name is in neither the installed set nor the chain (and the error reports both misses).

A --check violation is a successful report, not a failed command. Under --json the document still carries ok: true, and the verdict rides on this exit code alone. Same split as pkg-check-update(1).

FILES

/usr/share/pkg-licenses/<pkg>/
License text as the build installed it, at the paths [license] files records.
/var/lib/pkg/installed/
The manifests every answer is read from.
/etc/pkg.toml
The [license] policy that --check evaluates. See pkg.toml(5).

NOTES

Every judgement about an expression goes through the parser in pkg-format, never a test on the raw string. The difference between Apache-2.0 OR MIT and GPL-2.0-only AND MIT is the whole question. Denying Apache-2.0 leaves the first one installable under MIT, and makes no difference to the second. A denial that only looked at the list of ids would get both wrong, in opposite directions.

Not here yet: prompting before you accept a license, a cache of what you already accepted, and a gate that applies [license] at install time instead of after the fact. Those need a terminal and a record of decisions. This command only reports, and reporting needs neither.

SEE ALSO

pkg(1), pkg-info(1), pkg-list(1), pkg-query(1), pkg-check-update(1), pkg.toml(5), pkg-state(5), pkg-subpkgs(7)

docs/design/format.md, docs/design/sbom.md