Skip to content

Repository files navigation

License Compatibility Checker

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.

Contents

Quick start

Gradle

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 checkLicenses

checkLicenses also runs as part of ./gradlew check automatically (it's wired in as a standard verification task).

Maven

<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).

How it works

Dependency Resolution → License Discovery → Policy Evaluation → Reports / Build Failure
  1. Dependency resolution — each build tool's own adapter (GradleDependencyResolver / MavenDependencyResolver) resolves the project's dependency graph into a build-tool-agnostic Dependency list, honoring includeScopes/excludeScopes.

  2. 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:

    1. A syntactically-valid SPDX expression found directly in the POM/Gradle metadata license field
    2. Free-text POM license name, resolved through the policy's aliases: table
    3. Free-text Gradle module metadata license name (same alias resolution; see known limitations — the Gradle plugin doesn't currently populate this one)
    4. The jar's META-INF/MANIFEST.MF (Bundle-License, License, Implementation-License headers)
    5. A bundled LICENSE/LICENSE.txt/COPYING file, 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.

  3. 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 an unknown: action.

  4. Reports — a stable, sorted ReportModel is 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.

Writing a policy

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 identifier
  • allow / 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.
  • overrides are keyed by group:artifact (no version — an override applies to every version of that module) and always win over whatever discovery found.
  • aliases map exact, case-sensitive free-text strings (as they appear in a POM or Gradle metadata) to the SPDX identifier they mean.

Using a built-in Policy Pack

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>.

Writing your own Policy Pack

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.

Knowledge Packs

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.

Gradle configuration reference

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.

Maven configuration reference

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.

Report formats

  • 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, a summary of counts by status, and a dependencies array 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).

Project structure

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

Why maven-plugin isn't in the Gradle build

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.

Checking multiple repositories at once

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.sh

Run ./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.

Building and testing

# 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 verify

CI (.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.

Design decisions and known limitations

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-core built 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 no when: {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's allow: list matches that exact compound expression, not MIT and Apache-2.0 independently.

See docs/prompt2.md for the fuller record of these and other decisions made while building Knowledge Pack/Policy Pack support.

About

Library license compatibility checker

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages