Help us shape the future of Go! Take the 2026 Go Developer Survey

spdx

package
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 17, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

Documentation

Overview

Package spdx is the SPDX vocabulary: it normalizes license names onto SPDX identifiers, reads SPDX expressions, and answers questions about them.

Normalization

The identifiers a classifier produces and the identifiers upstreams declare are both, routinely, not SPDX. licenseclassifier emits names that were never SPDX (Apache-with-LLVM-Exception, BSD-0-Clause) and names SPDX has since deprecated (LGPL-3.0, GPL-2.0-with-classpath-exception). Ecosystem metadata carries free prose and bare URLs. Comparing those against a declared expression without normalizing first reports disagreements that are only spelling, and publishing them unnormalized puts non-SPDX text in a licenseConcluded field.

One class cannot be normalized, only reported: the GPL family's -only versus -or-later distinction. SPDX ships identical text for both forms, so no classifier can tell them apart from a license file. Normalize reports those identifiers as needing a grant, and the caller resolves it from the notice in the component's own source.

Expressions

Parse reads an expression into a form that can be asked two questions, so that no consumer hand-rolls SPDX semantics from string matching.

Identifiers is the exposure view: every identifier the expression names, including one from each OR branch, because this package does not elect a branch. A license combined with a WITH exception stays one identifier, since the exception is what makes the terms bearable and dropping it would report obligations the consumer does not have.

Satisfies is the evaluation view: whether the expression can be met by a set of identifiers the caller accepts. An OR is satisfied by any one branch and an AND by all of them, which is the rule substring matching gets wrong in the direction that matters - "MIT OR GPL-3.0-only" is acceptable to a consumer that accepts MIT, and reads as unacceptable to anything scanning for the string "GPL".

What it will not do

It does not rank families by restrictiveness. Family reports which family an identifier belongs to; which families a consumer will accept, and in what order, is a policy question with a different owner.

It does not resolve a bare family name. "GPL" and "BSD" name no particular license, and mapping one to a guess would invent a fact the metadata does not contain.

Index

Examples

Constants

View Source
const (
	OperatorAnd  = "AND"
	OperatorOr   = "OR"
	OperatorWith = "WITH"
)

Operators are the SPDX expression keywords.

View Source
const RefPrefix = "LicenseRef-"

RefPrefix is the SPDX prefix for a license with no SPDX identifier. See https://spdx.github.io/spdx-spec/SPDX-license-expressions/.

Deriving the name for an unrecognized license text is contenthash's LicenseRef, not this package's: the name is a digest, and it is one of two durable keys that have to share one normalization and be versioned together with the other. What lives here is the vocabulary - the prefix, and what counts as a ref.

Variables

This section is empty.

Functions

func Canonicalize

func Canonicalize(expr string) string

Canonicalize rewrites a declared license value into valid SPDX, so that what gets published is an expression rather than whatever prose upstream wrote.

Ecosystem metadata is mostly not SPDX. A pom names "The Apache Software License, Version 2.0", an npm field carries a bare URL, and Cargo's deprecated "MIT/Apache-2.0" means a choice. Left alone, none of those compares equal to the identifiers classified from the same component's files, so every one of them reads as a disagreement that is not there.

Only unambiguous rewrites are made. Each operand is mapped through SPDX's own name and URL data, and an operand that does not resolve is left exactly as it was rather than guessed at - a bare "GPL" names no particular license, and inventing one would be worse than reporting the prose.

func Family

func Family(id string) string

Family reports the license family an identifier belongs to, or "" for an identifier with no family: a LicenseRef, whose terms are whatever its text says, or an unparseable string.

The family is the identifier up to its first version segment, so BSD-3-Clause, BSD-2-Clause-Views and 0BSD are all BSD, and GPL-2.0-only and GPL-3.0-or-later are both GPL. An identifier carrying a WITH exception takes the family of the license, since an exception narrows terms rather than replacing them.

Families are deliberately unordered. A consumer deciding that a change from one family to another is worth stopping a merge over is applying a policy, and this package holds no policy: it says which family, never which is worse.

func FromText

func FromText(s string) (string, bool)

FromText maps a free-text license string or a license URL onto an SPDX identifier, using SPDX's own name and seeAlso data rather than a hand-written synonym list.

Ecosystem metadata is full of these: a pom naming "The Apache Software License, Version 2.0", an npm field carrying a bare URL. What it deliberately will not do is resolve a bare family name - "GPL", "BSD" - because those name no particular license and guessing one would invent a fact.

Example

FromText resolves the prose and URLs that ecosystem manifests carry.

package main

import (
	"fmt"

	"chainguard.dev/license/spdx"
)

func main() {
	for _, s := range []string{"The Apache Software License, Version 2.0", "https://www.eclipse.org/legal/epl-2.0/", "GPL"} {
		if id, ok := spdx.FromText(s); ok {
			fmt.Printf("%q -> %s\n", s, id)
		} else {
			fmt.Printf("%q -> unresolved\n", s)
		}
	}
}
Output:
"The Apache Software License, Version 2.0" -> Apache-2.0
"https://www.eclipse.org/legal/epl-2.0/" -> EPL-2.0
"GPL" -> unresolved

func HeaderGrant

func HeaderGrant(name, variant string) (string, bool)

HeaderGrant reports the version grant a classifier header match states.

The classifier names every variant of a GPL header after the license alone, so a match on the "or later" wording and a match on the bare-version wording both come back as "GPL-2.0". The variant is what distinguishes them, and the generated table records which is which.

func Identifiers

func Identifiers(expr string) []string

Identifiers returns the identifiers a license expression names.

It prefers a parse, and falls back to reading the identifiers out of the string when it is not valid SPDX. That fallback is what makes the function usable on declared metadata, which is frequently not an expression at all: the identifiers a malformed declaration names are still worth comparing against what was detected, and refusing to read them would report every such component as declaring nothing.

The fallback reads tokens rather than structure, so a WITH pair inside an otherwise malformed string comes back as two identifiers instead of one. Run Canonicalize first where that matters: an expression that parses keeps its exception attached.

func IsDeprecated

func IsDeprecated(id string) bool

IsDeprecated reports whether an identifier is a deprecated SPDX identifier.

func IsRef

func IsRef(id string) bool

IsRef reports whether an identifier is a LicenseRef rather than a listed SPDX license.

It is the test that keeps a real license from being forced onto the nearest listed one. A proprietary EULA has no SPDX identifier and never will, and classifying it as the closest match would state terms its author never granted.

func ListVersion

func ListVersion() string

ListVersion reports the SPDX license list release the vendored data came from.

func Ref

func Ref(name string) string

Ref renders a name as a LicenseRef identifier, adding the prefix only when it is not already there, so that a curated name and a bare one both arrive in the same form.

func Tag

func Tag(s string) (string, bool)

Tag extracts an SPDX-License-Identifier expression from source text.

Example

Tag reads the SPDX-License-Identifier convention, which the classifier does not recognize at all.

package main

import (
	"fmt"

	"chainguard.dev/license/spdx"
)

func main() {
	id, ok := spdx.Tag("/* SPDX-License-Identifier: LGPL-2.1-or-later */")
	fmt.Println(id, ok)
}
Output:
LGPL-2.1-or-later true

func WithoutGrant

func WithoutGrant(id string) string

WithoutGrant returns an identifier with any version grant removed.

It exists for the comparison a consumer cannot otherwise make honestly. The license text of GPL-2.0-only and GPL-2.0-or-later is byte-identical, so a classifier can never produce the suffix; when a declaration states one and detection could not, the two are not in conflict, and comparing them verbatim reports a disagreement that is not there. Removing the grant from both sides is what makes them comparable.

A WITH exception is kept, because an exception is not a grant: it changes what the license permits rather than which versions of it apply.

Types

type Expression

type Expression struct {
	// contains filtered or unexported fields
}

Expression is a parsed SPDX license expression.

It is opaque because the two questions consumers ask of an expression are Identifiers and Satisfies, and both are easy to get wrong from the outside: an exposure view that drops OR branches understates what a consumer is subject to, and an evaluation that treats OR as AND rejects an expression the consumer accepts.

func Parse

func Parse(expr string) (Expression, error)

Parse reads an SPDX license expression.

Operator precedence follows the specification: "+" binds tightest, then WITH, then AND, then OR, with parentheses grouping. Identifiers are checked for shape but not for membership of the license list, because a valid expression may name a LicenseRef this package has never heard of, and because refusing an unlisted identifier here would leave the caller with no way to read an expression it still has to record faithfully.

Callers holding ecosystem metadata rather than an expression should run Canonicalize first: a pom naming "The Apache Software License, Version 2.0" is not an expression and will not parse.

func (Expression) Identifiers

func (e Expression) Identifiers() []string

Identifiers returns the identifiers the expression exposes a consumer to, sorted and deduplicated.

Every OR branch contributes, because this package does not elect a branch: which branch a consumer relies on is their choice to record, and an exposure view that guessed one would hide the terms of the other. A WITH form stays a single identifier for the same reason - "GPL-2.0-only WITH Classpath-exception-2.0" is not GPL-2.0-only, and splitting it would report an obligation the exception removes.

func (Expression) Satisfies

func (e Expression) Satisfies(accept func(id string) bool) bool

Satisfies reports whether the expression is met when accept holds for an identifier.

An OR is satisfied by any one branch and an AND by all of them. accept is called with the whole identifier, WITH exception included, so a caller that accepts a license only under an exception can say so.

func (Expression) String

func (e Expression) String() string

String renders the expression, parenthesizing only where precedence requires it, so that re-parsing the result yields the same tree.

type Resolution

type Resolution struct {
	// ID is a current SPDX identifier or expression.
	ID string
	// GrantBase names a GPL-family license whose SPDX identifier depends on a
	// version grant the license text cannot express. Only and OrLater are the
	// two identifiers it could be; the caller decides between them from the
	// notice in the component's own source.
	GrantBase string
	Only      string
	OrLater   string
	// Ref is a real license with no SPDX identifier. It travels as a
	// LicenseRef- rather than being forced onto the nearest listed license.
	Ref string
}

Resolution is what a license identifier normalizes to. Exactly one of ID, GrantBase and Ref is set.

func Normalize

func Normalize(name string) Resolution

Normalize maps a license name produced by a classifier onto SPDX.

Names that are already current SPDX identifiers pass through. The rest are resolved through the generated table, which is derived from the classifier's own vocabulary rather than hand-written, so it cannot drift silently when the dependency is upgraded.

Example

Normalize maps a classifier's license name onto SPDX, reporting the three outcomes a caller has to handle.

package main

import (
	"fmt"

	"chainguard.dev/license/spdx"
)

func main() {
	for _, name := range []string{"Apache-2.0", "BSD-0-Clause", "LGPL-3.0", "Commons-Clause"} {
		r := spdx.Normalize(name)
		switch {
		case r.NeedsGrant():
			fmt.Printf("%-14s -> %s or %s (grant undecided)\n", name, r.Only, r.OrLater)
		case r.ID != "":
			fmt.Printf("%-14s -> %s\n", name, r.ID)
		default:
			fmt.Printf("%-14s -> LicenseRef-%s\n", name, r.Ref)
		}
	}
}
Output:
Apache-2.0     -> Apache-2.0
BSD-0-Clause   -> 0BSD
LGPL-3.0       -> LGPL-3.0-only or LGPL-3.0-or-later (grant undecided)
Commons-Clause -> LicenseRef-Commons-Clause

func (Resolution) Known

func (r Resolution) Known() bool

Known reports whether the identifier resolved to SPDX at all.

func (Resolution) NeedsGrant

func (r Resolution) NeedsGrant() bool

NeedsGrant reports whether the identifier is a GPL-family form whose -only or -or-later suffix is still undecided.

Directories

Path Synopsis
internal
cmd/gen command
Command gen writes the generated SPDX normalization tables.
Command gen writes the generated SPDX normalization tables.
gen
Package gen derives the SPDX normalization tables from two sources of truth: the vendored SPDX license list, and the license names licenseclassifier can actually emit.
Package gen derives the SPDX normalization tables from two sources of truth: the vendored SPDX license list, and the license names licenseclassifier can actually emit.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL