A Gradle plugin and Maven plugin that validate your project's dependency licenses against a configurable policy, and fail the build when a dependency's license isn't allowed.
Both plugins share a single build-tool-agnostic engine (core): dependency
resolution → license discovery → policy evaluation → reports, so the two
build tools behave identically given the same policy and the same
dependencies.
Not legal advice. This tool enforces the policy you configure; it doesn't determine your legal obligations under any license.
- Quick start
- How it works
- Writing a policy
- Using a built-in Policy Pack
- Knowledge Packs
- Gradle configuration reference
- Maven configuration reference
- Report formats
- Checking multiple repositories at once
- Project structure
- Building and testing
- Design decisions and known limitations
plugins {
id("com.github.curious-odd-man.llcc") version "1.0.0"
}
licenseChecker {
policyFile.set(file("licenses.yml")) // default: <project dir>/licenses.yml
reportFormats.set(listOf("json")) // console always runs in addition to this
}./gradlew checkLicensescheckLicenses also runs as part of ./gradlew check automatically (it's
wired in as a standard verification task).
<build>
<plugins>
<plugin>
<groupId>com.github.curious-odd-man.llcc</groupId>
<artifactId>license-checker-maven-plugin</artifactId>
<version>1.0.0</version>
<configuration>
<policyFile>${project.basedir}/licenses.yml</policyFile>
<reportFormats>
<reportFormat>json</reportFormat>
</reportFormats>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>mvn com.github.curious-odd-man.llcc:license-checker-maven-plugin:1.0.0:check(or just mvn verify, since the check goal binds to the verify phase by
default once the execution above is configured).
Dependency Resolution → License Discovery → Policy Evaluation → Reports / Build Failure
-
Dependency resolution — each build tool's own adapter (
GradleDependencyResolver/MavenDependencyResolver) resolves the project's dependency graph into a build-tool-agnosticDependencylist, honoringincludeScopes/excludeScopes. -
License discovery — for each dependency, up to five sources are tried in priority order, and every one of them is consulted (not just the first hit), so a disagreement between sources is reported as a conflict even though evaluation proceeds with the highest-priority answer:
- A syntactically-valid SPDX expression found directly in the POM/Gradle metadata license field
- Free-text POM license name, resolved through the policy's
aliases:table - Free-text Gradle module metadata license name (same alias resolution; see known limitations — the Gradle plugin doesn't currently populate this one)
- The jar's
META-INF/MANIFEST.MF(Bundle-License,License,Implementation-Licenseheaders) - A bundled
LICENSE/LICENSE.txt/COPYINGfile, matched against a small set of known license text fingerprints
A manual
overrides:entry (see below) isn't part of this discovery chain at all — it's applied afterwards, during policy evaluation, and always wins over whatever discovery found. -
Policy evaluation — against either a hand-written policy file or a Policy Pack: the resolved license is checked against
allow/deny/warn(exact string match; a compound expression like"MIT OR Apache-2.0"needs to appear in the policy verbatim — it isn't decomposed into its parts). No match falls back to anunknown:action. -
Reports — a stable, sorted
ReportModelis rendered into every requested format. Console output always runs (it's what drives the build failure); JSON, XML, Markdown, and HTML are written to disk when requested.
Every stage above lives in core and has no compile-time dependency on
either build tool's API — only the two adapters ("how do I resolve
dependencies" / "how do I build a discovery context from a real resolved
artifact") are build-tool-specific.
A policy is a single YAML file (licenses.yml by default). This is the
"bring your own policy" path — see
Using a built-in Policy Pack below for a
one-line alternative that ships with organization-wide compatibility
decisions already written and explained for you.
allow:
- Apache-2.0
- MIT
- MIT OR Apache-2.0 # compound expressions need an exact match too
deny:
- GPL-3.0-only
warn:
- MPL-2.0 # neither pass nor fail the build; flagged for review
unknown: unknown # fail | warn | allow | unknown (default: unknown)
overrides:
org.foo:legacy-lib:
license: Apache-2.0 # force a specific license, bypassing discovery entirely
org.bar:vendored-lib:
ignore: true # exclude this dependency from evaluation (still shown in reports)
aliases:
Apache License 2.0: Apache-2.0 # map a free-text POM license name to its SPDX identifierallow/deny/warn/unknown's value / every alias target must be a syntactically valid SPDX expression — invalid entries fail policy loading immediately, with a clear error, rather than silently never matching anything.overridesare keyed bygroup:artifact(no version — an override applies to every version of that module) and always win over whatever discovery found.aliasesmap exact, case-sensitive free-text strings (as they appear in a POM or Gradle metadata) to the SPDX identifier they mean.
A Policy Pack is a bundled, versioned, inheritable alternative to writing
your own licenses.yml — each rule pairs an allow/warn/deny/unknown
decision with the pack author's own explanation, and reports show which
pack (and, if inherited, which ancestor pack) actually produced each
decision.
licenseChecker {
policyPack.set("apache-2.0") // instead of policyFile
}<configuration>
<policyPack>apache-2.0</policyPack> <!-- instead of <policyFile> -->
</configuration>policyPack and policyFile are mutually exclusive — set exactly one.
Five built-in packs ship with the plugin: mit, apache-2.0,
bsd-2-clause, bsd-3-clause, mpl-2.0. Each classifies the same 11
licenses (MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, MPL-2.0, EPL-2.0,
LGPL-2.1, LGPL-3.0, GPL-2.0, GPL-3.0, AGPL-3.0) relative to that pack's own
project license — permissive licenses are allowed, other weak-copyleft
licenses are warned on, and strong/network copyleft licenses are denied by
default.
Pin a specific version instead of the latest with id@version, e.g.
policyPack.set("apache-2.0@1") / <policyPack>apache-2.0@1</policyPack>.
An organization can publish its own Policy Pack that extends a built-in
one, overriding or adding rules:
id: acme-corp
name: Acme Corp Compatibility Policy
version: 1
extends: apache-2.0
licenses:
LGPL-3.0:
action: deny
reason: Company policy prohibits weak copyleft, stricter than the base Apache-2.0 policy.
Internal-Approved:
action: allow
reason: Pre-approved internal license for Acme-authored components.A child pack's rule for a given license replaces the same rule inherited
from a parent; anything the child doesn't override still applies from the
parent. Every rule's reason is required — the whole point of a Policy
Pack over a flat licenses.yml is that every decision comes with an
explanation, not just a yes/no.
Facts vs. decisions. A Policy Pack rule (action + reason) is a
decision your organization made; it deliberately doesn't carry facts
about the license itself (whether it's copyleft, OSI-approved, patent
grant, and so on) — those live in a separate
Knowledge Pack instead. If you're writing a custom pack
and want to explain why a license is treated a certain way, look up its
facts in the bundled spdx-core Knowledge Pack (or your own) and put the
reasoning into your rule's reason: text — don't try to encode
"copyleft: strong" anywhere in a Policy Pack; that split is deliberate, not
missing functionality.
A Knowledge Pack is a bundled database of facts about licenses — SPDX
identifier, common free-text aliases, license family, OSI/FSF approval,
copyleft strength, patent grant, obligations, reference links — and
never an allow/deny decision. One ships built in, spdx-core, covering
the same 11 licenses the built-in Policy Packs classify.
Knowledge Packs currently do one concrete thing for you: supply the alias
map ("Apache License 2.0" → Apache-2.0, "The MIT License" → MIT, and so
on) that the Policy Pack path uses to
normalize free-text license names found in dependency metadata — the same
job a flat policy's own aliases: table does for that path.
licenseChecker {
policyPack.set("apache-2.0")
knowledgePacks.set(listOf("acme-corp")) // extra aliases layered on top of spdx-core
}Extra Knowledge Packs are distributed as a jar on the buildscript/plugin
classpath with resources at knowledge-packs/<id>/<version>.yml — the same
layout the bundled spdx-core pack itself uses. Has no effect on the
policyFile path, which uses its own YAML-embedded aliases: instead.
All properties live under the licenseChecker { } extension block.
| Property | Type | Default | Notes |
|---|---|---|---|
projectLicense |
Property<String> |
unset | Informational only in this version — not yet used in evaluation; reserved for a future project-vs-dependency compatibility check. |
policyFile |
RegularFileProperty |
<project dir>/licenses.yml, unless policyPack is set |
Mutually exclusive with policyPack — setting one clears the other's default. Must resolve to an existing file, or checkLicenses fails with a clear configuration error. |
policyPack |
Property<String> |
unset | A built-in or published Policy Pack id, e.g. "apache-2.0", or "apache-2.0@1" to pin a version. See Using a built-in Policy Pack. Mutually exclusive with policyFile. |
knowledgePacks |
ListProperty<String> |
[] |
Extra Knowledge Packs (same "id" / "id@version" form) for alias resolution alongside the built-in spdx-core pack. Only used with policyPack. |
failOnUnknown |
Property<Boolean> |
false |
When true, forces every otherwise-unknown dependency to FAIL, regardless of what the policy's (or pack's) own unknown: says. |
includeScopes |
ListProperty<String> |
[] (= all scopes) |
One or more of compile, runtime, test, provided, optional. |
excludeScopes |
ListProperty<String> |
[] (= none excluded) |
Same values as includeScopes. |
reportFormats |
ListProperty<String> |
[] |
One or more of json, xml, markdown (or md), html. Console always runs in addition, whether or not it's listed. |
outputDirectory |
DirectoryProperty |
build/reports/license-checker |
Where file-based report formats are written, as license-report.<ext>. |
checkLicenses is a real Gradle task with @Input/@InputFile/
@OutputDirectory-annotated properties (plus the resolved dependency
classpath as an extra lazily-registered input), so Gradle can mark it
UP-TO-DATE and skip re-running it when neither the policy file, the
config above, nor the resolved dependencies have changed.
All parameters can be set via <configuration> or the matching
-DlicenseChecker.* system property.
| Parameter | Property | Default | Notes |
|---|---|---|---|
projectLicense |
licenseChecker.projectLicense |
unset | Same "informational only" caveat as Gradle's. |
policyFile |
licenseChecker.policyFile |
unset | No longer defaults to ${project.basedir}/licenses.yml — mutually exclusive with policyPack, and a fixed default would make policyPack impossible to use. Projects relying on the old default must now set <policyFile>${project.basedir}/licenses.yml</policyFile> explicitly, or switch to <policyPack>. |
policyPack |
licenseChecker.policyPack |
unset | A built-in or published Policy Pack id, e.g. apache-2.0, or apache-2.0@1 to pin a version. See Using a built-in Policy Pack. Mutually exclusive with policyFile. |
knowledgePacks |
licenseChecker.knowledgePacks |
empty | Extra Knowledge Packs (same id / id@version form) for alias resolution alongside the built-in spdx-core pack. Only used with policyPack. |
failOnUnknown |
licenseChecker.failOnUnknown |
false |
|
includeScopes |
licenseChecker.includeScopes |
empty (= all scopes) | |
excludeScopes |
licenseChecker.excludeScopes |
empty (= none excluded) | |
reportFormats |
licenseChecker.reportFormats |
empty | Console always runs in addition. |
outputDirectory |
licenseChecker.outputDirectory |
${project.build.directory}/license-checker |
|
skip |
licenseChecker.skip |
false |
Skips the goal entirely — no resolution, no reports. Gradle has no direct equivalent; use -x checkLicenses instead. |
The check goal binds to the verify lifecycle phase by default and
requires dependency resolution up to test scope.
- Console — one line per dependency (✔/⚠/?/✖ + license), a build verdict, violation counts, then a "Details" section (dependency, license, reason, dependency path, suggested remediation) for anything that isn't a clean pass. This is what's printed to the build log and drives the build failure.
- JSON — a stable, pretty-printed document: overall
status, asummaryof counts by status, and adependenciesarray with every field from the console details section. - XML — the same data via plain StAX (no XML binding framework).
- Markdown — a summary table, a per-dependency table, and an expanded "Details" section, suitable for pasting into a PR comment or CI summary.
- HTML — a single, self-contained file (inline CSS, no external resources), safe to open straight from disk or publish as a CI artifact.
All five are pure functions of the same underlying report model: identical input always produces byte-identical output in every format (no timestamps, no non-deterministic ordering).
core/ Build-tool-agnostic engine (Gradle module)
src/main/java/…/core/
dependency/ Coordinates, scope, the Dependency model
resolution/ DependencyResolver contract (adapters implement it)
discovery/ The 5-source license discovery chain
license/ License / LicenseSource model
spdx/ Minimal hand-rolled SPDX expression parser
policy/ Flat Policy model + YAML loader (licenses.yml)
knowledgepack/ Knowledge Pack model, loader, registry (facts only)
policypack/ Policy Pack model, loader, inheritance resolver,
registry, standalone validator (decisions only)
pack/ Shared PackSource abstraction (classpath/filesystem)
used by both pack kinds for distribution
evaluation/ Policy evaluation engine (PolicyDecisionSource
unifies the flat-Policy and Policy Pack paths)
report/ Report model + the 5 report generators
engine/ LicenseCheckEngine — ties everything together
config/ LicenseCheckerConfig, the shared config surface
src/main/resources/
knowledge-packs/spdx-core/ Bundled Knowledge Pack (11 licenses' facts)
policy-packs/ 5 bundled Policy Packs (mit, apache-2.0,
bsd-2-clause, bsd-3-clause, mpl-2.0)
src/testFixtures/java/…/core/fixtures/
Shared fixture dependency roster + fake repo builder,
reused by core's own tests and both plugins' functional tests
gradle-plugin/ Gradle module: extension, checkLicenses task, adapters
maven-plugin/ STANDALONE Maven reactor (own pom.xml) — see below
.github/workflows/ci.yml Two CI jobs: Gradle reactor, Maven reactor
docs/ Product requirements, implementation design, build plan
maven-plugin needs real Maven Mojo annotation processing
(maven-plugin-plugin), so it's its own Maven reactor with its own
pom.xml, not a Gradle subproject. It depends on core (including its
shared test fixtures, published under a test-fixtures classifier) as a
regular Maven artifact — see Building and testing
for how that dependency actually gets resolved.
scripts/run-license-check.sh sequentially clones a list of repositories
and runs whichever license-checker command matches each one's build tool
(Gradle's checkLicenses, or the Maven plugin's check goal) — useful for
auditing a whole organization's repos from one place rather than wiring the
plugin into each one's CI individually.
cp scripts/repos.txt.example scripts/repos.txt # one git URL (+ optional branch) per line
./scripts/run-license-check.shRun ./scripts/run-license-check.sh --help for options (custom repo list
location, clone destination, continue-past-failures, reusing existing
checkouts instead of re-cloning, and overriding the Gradle task / Maven
goal invoked). The target repositories need the plugin applied/configured
themselves — this script just automates cloning them and invoking the
right command for each.
# core + gradle-plugin (unit tests, Gradle TestKit functional tests)
./gradlew build
# maven-plugin — core must be published to ~/.m2 first, since maven-plugin
# is a separate Maven reactor that resolves it as a regular dependency
./gradlew :core:publishToMavenLocal
cd maven-plugin && mvn verifyCI (.github/workflows/ci.yml) runs both of the above, across JDK 11/17/21,
in separate jobs — the Maven job publishes core to Maven local before
running mvn verify, exactly as above.
A few deliberate scope decisions, documented here so they're easy to find rather than buried in code comments:
- Knowledge Packs are facts only, Policy Packs are decisions only. A
Knowledge Pack (
spdx-corebuilt in) never contains an allow/deny decision, enforced at the type level; a Policy Pack rule never carries license facts like copyleft strength — see "Writing your own Policy Pack"'s "Facts vs. decisions" note. This split is deliberate, not a missing feature in either direction. - Policy Packs support flat rules only, not conditional ones. A rule is
always
{license id: {action, reason}}— there's nowhen: {copyleft: strong}-style conditional matching against Knowledge Pack facts yet. - No SBOM input. Dependencies come from build-tool resolution only, not from a CycloneDX/SPDX document.
- Gradle Module Metadata license text isn't resolved. Unlike a
dependency's POM (which Gradle can resolve as a single artifact via
ArtifactResolutionQuery), its Gradle Module Metadata license field has no equivalent single-artifact resolution path in the public Gradle API. POM, manifest, and bundled-license-file discovery all still work normally for Gradle projects. - No online license metadata providers, and no Maven-coordinate or Git-repository Policy/Knowledge Pack distribution. Everything resolves from already-downloaded local artifacts or the classpath/filesystem; no extra network calls. Extra packs ship as a jar on the classpath (see Knowledge Packs) or from a filesystem directory, not from a remote coordinate.
- Compound SPDX expressions need an exact policy match.
"MIT OR Apache-2.0"in a policy'sallow:list matches that exact compound expression, notMITandApache-2.0independently.
See docs/prompt2.md for the fuller record of these and other decisions
made while building Knowledge Pack/Policy Pack support.