Computing History · Open Source History

SPDX License Expressions and Machine-Readable Provenance

How a single identifier grammar replaced prose license statements, what the operators and version suffixes encode, and why the declared license of a package and the licenses of its files routinely disagree.

A license statement written as prose is unusable by software. It cannot be compared, sorted, joined against a policy list, or aggregated across a dependency graph without a human reading it each time. The obvious remedy is to write the license as a short string from a controlled vocabulary, and the vocabulary that settled is the SPDX license list: a dated data set of license identifiers, each with a canonical short form, the full text it stands for, and flags recording whether the license is deprecated or whether its text has been replaced by a later version.

The vocabulary is closed. A string is an SPDX identifier only if it appears in the list the tooling was built against, which is why the list carries a version number and tools record which version they resolved against. That closure is what allows a consumer to decide, without parsing legal text, that a dependency is acceptable, that two dependencies share a license, or that a combination is impossible.

The grammar of an expression

An expression is built from identifiers, the exception operator WITH, the conjunction and disjunction operators AND and OR, and parentheses. WITH attaches exactly one exception to exactly one license, as in GPL-2.0-or-later WITH Classpath-exception-2.0 or GPL-3.0-only WITH GCC-exception-3.1; exceptions exist because a license with an exception is a different grant from the same license without it, and treating the pair as one token would lose that.

The choice of suffix on versioned licenses is load-bearing, not cosmetic. GPL-2.0-only and GPL-2.0-or-later describe different grants: the first pins the work to one version of the license, the second offers the recipient the option of any later version. Earlier shorthand for the same distinction used a bare identifier or a trailing plus sign, forms that are now deprecated because a reader cannot tell from GPL-2.0 alone which scope the grant had. The distinction is exactly the one that decides whether a project can move to a later license without asking its contributors, as described in license compatibility.

OR offers the recipient a choice and AND demands that every named license be satisfied. The difference is not stylistic: MIT OR Apache-2.0, the common declaration for a library, means a redistributor may take the code under either license and must not be told that both apply, while GPL-2.0-only AND GPL-3.0-only is unsatisfiable for a single work, because no distribution can hold both conditions at once. An expression that conjoins two mutually incompatible licenses is not a stricter statement of the same thing; it is a statement with no solution, and its appearance usually means the generator treated a set of file-level licenses as obligations when it should have recorded a per-file split.

When licenses are absent from the list, the grammar provides a local namespace: LicenseRef- followed by an identifier chosen by the producer. The reference is only resolvable if the same document also supplies the license text for it, which is why an expression containing a LicenseRef- and no accompanying text is worse than free prose. SPDX documents historically also provide a document namespace prefix for referring to identifiers defined in another document.

{
  "name": "libexample",
  // Package-level declaration: the recipient may choose either license.
  "license": "MIT OR Apache-2.0",
  // A file that had to stay under file-scope copyleft is recorded per file
  // rather than folded into the package expression.
  "files": [
    { "path": "src/vendor/mpl_code.c", "license": "MPL-2.0" }
  ]
}

Declared, concluded, and detected

SPDX distinguishes the license a producer claims for an artifact from the license a reviewer concluded after inspection. The package fields carry a declared license and a concluded license, and file entries carry a list of licenses found in the file; the concluded value may be the same expression, a narrowed one, or something the declared value did not mention. The distinction exists because declarations drift. A manifest is written once and rarely revisited; files are added by many hands; vendored directories carry their own notices; generated sources have no header at all. A scanner therefore reports what it detected, a human may conclude something different, and the two fields make that disagreement explicit instead of hiding it in a footnote.

The per-file convention that pairs with this is SPDX-License-Identifier: comments in source files, alongside SPDX-FileCopyrightText: lines for attribution, with the full license texts kept in a separate directory. The REUSE specification standardizes that layout so that a file is self-describing and the license of any single file can be recovered without consulting the package manifest. For a distributed artifact, file-level truth is what matters, because that is the granularity at which the license grant is made, as discussed in what a license does to a copyright default.

The same expressions travel inside bill-of-materials documents, both the SPDX document format and the CycloneDX format, which is how an inventory of a built container image can record a license per component. SPDX itself was standardized as an international standard in 2021 on the basis of its version 2.2 specification, which is why the identifier grammar and the document fields are stable enough to be embedded in build outputs rather than only in human-run audits.

The list is versioned data, not a constant

Identifiers are added to the list when licenses appear, and they are deprecated when the license they name is replaced. The reworking of the versioned GPL identifiers is the clearest case: the older shorthand forms, which left the version scope of the grant unstated, gave way to explicit suffixes that record whether the grant is pinned to one version or extends to later ones. A tool built before that change and a tool built after it will both accept the old strings, but only the newer one has a vocabulary in which the distinction can be written down at all, which is why the list carries a version and why a document records the version it was produced with.

The practical consequence is that an identifier is a claim about a specific version of a specific list. Two systems exchanging inventories have to agree on a vocabulary: the older system sees an identifier it does not know and either rejects the expression or downgrades it to a local reference, and the newer system sees a deprecated identifier and cannot tell whether the producer meant one version or several. Conventions such as a per-file header line and a project-level statement of which list version applies exist to make that mismatch detectable rather than silent.

Exceptions are versioned the same way and are named separately from licenses, because an exception modifies a grant rather than standing on its own. An expression that attaches an exception to a license family is relying on the pairing having existed in the list the producer used, and a tool that does not know the exception reduces the expression to the unmodified license, which is a stricter statement than the file’s header makes.

Failure modes that survive validation

Expressions are syntax-checkable, and a validated expression can still be wrong. Four cases recur.

Declaration drift is the common one. A package declares MIT, one vendored directory inside it carries a copyleft header, and the declaration is false for the artifact that ships. Consumers that trust the declared value inherit the error and pass it downstream, where it becomes someone else’s provenance problem.

A second case is a narrowed grant. A distributor that resolves MIT OR Apache-2.0 to MIT for its own release has made the choice the expression offered, and its recipients receive the narrower grant. If the distributor’s build also relies on the patent terms of the Apache grant, the choice silently discarded them.

A third is version hygiene: a file whose header says “either version 2 of the License, or (at your option) any later version” recorded in metadata as GPL-2.0-only. One of the two statements is wrong, and the metadata one removes an upgrade path that the copyright holder granted.

A fourth is over-conjunction. Tooling that merges the file-level licenses of a package into a single expression, when the package’s own grant is a choice or a per-file split, produces AND chains that no distributor can satisfy. The fix is not to relax the checker but to record the licenses at the granularity at which they were granted, which is one reason a tree’s license inventory cannot be reduced to one string.

What the identifiers cannot express

The grammar covers which licenses apply. It does not record whether the person who wrote the header had the right to do so, whether a contributor’s employer holds the copyright, or whether a package contains code whose provenance was never recorded. Those are questions about the history of the files, and no identifier resolves them. The practical consequence is that a complete license expression is evidence about claims, not proof about rights, and the machinery that makes the claims reliable is the contribution paperwork covered in contribution agreements.

References

  1. SPDX Specification, version 2.3 — SPDX Project, Linux Foundation
  2. SPDX License List — SPDX Project
  3. ISO/IEC 5962:2021, Information technology — SPDX Specification — ISO/IEC
  4. REUSE Specification — REUSE project
  5. CycloneDX Bill of Materials Specification — OWASP Foundation