Does Yocto’s INCOMPATIBLE_LICENSE Actually Block Copyleft Packages?

GPL compliance failures in embedded Linux are usually discovered in shipped firmware, not in build logs. That is the right frame for evaluating INCOMPATIBLE_LICENSE: it is a build-time recipe filter, not a compliance certificate. A clean Yocto build tells you the build system believed it excluded certain declared licenses. It does not tell you what is actually in the rootfs, the kernel image, or the SPI flash.

What the variable actually does

INCOMPATIBLE_LICENSE is evaluated during recipe parsing. In the OpenEmbedded implementation captured in a public poky commit, the check lives in license.bbclass as a function called incompatible_license(), invoked from base.bbclass. When a recipe’s declared LICENSE matches an unwanted pattern, the parser raises SkipPackage:

raise bb.parse.SkipPackage("incompatible with license %s" % this_license)

That is the whole enforcement mechanism at this layer: a parse-time skip. It operates on the LICENSE string the recipe declares. It does not open binaries, inspect link maps, or read the rootfs.

How the matching works

The matching logic in that commit is worth reading closely because it explains both the variable’s usefulness and its limits. The function builds a list of unwanted patterns from INCOMPATIBLE_LICENSE and from SPDXLICENSEMAP entries, then tests each candidate license with fnmatch:

if any(fnmatch(license, pattern) for pattern in dont_want_licenses):
    return False

Three details matter for anyone using this as a compliance control:

  • Pattern matching, not exact equality. fnmatch patterns mean a value like GPL-3.0* can sweep in multiple variants. That is convenient for excluding a family of licenses, but it also means the result depends on how the recipe author spelled the license and how the mapping table resolves it.
  • OR expressions are resolved by choosing the acceptable branch. The commit’s choose_licenses() helper returns the first operand only if every license in it is acceptable; otherwise it falls through to the second. So a dual-licensed recipe can survive the gate by selecting the non-copyleft branch. That is correct behavior for a filter, but it means the gate’s decision is only as good as the declared expression.
  • The trailing + is stripped unless the unwanted pattern itself contains one. The commit comment explains the intent: if you exclude a license generically, you want to exclude its + (or-later) form as well. This is a heuristic about license text, not a legal determination.

The 6.1 change makes the gate stricter — for declared licenses

Yocto 6.1 moved LICENSE to SPDX License Expressions. The migration notes are explicit about the consequence for this variable: anything listed in INCOMPATIBLE_LICENSE matches a single license, and it also matches the left-hand license side of a WITH expression. The practical effect is that GPL-3.0-or-later WITH SomeException now matches a GPL-3.0* exclusion, where the older bespoke-license syntax might have slipped past. To allow a specific exception, the documentation says the exception must be listed in INCOMPATIBLE_LICENSE_EXCEPTIONS.

This is a real improvement in matching fidelity. It is still matching against a string in a recipe.

Why a deliberately non-compliant image can still be built

The gate cannot see anything that does not pass through a recipe’s declared LICENSE. Several paths put code into an image without that metadata being the thing the gate evaluates:

  • Files copied in during do_install or rootfs post-processing. A vendor can place a binary or script into the rootfs through a recipe task or a postprocess command without that artifact being represented as a separately licensed package. INCOMPATIBLE_LICENSE never evaluates it because there is no recipe license string for it to match.
  • Statically linked copyleft code inside a proprietary binary. If a GPL library is linked into a closed application, the recipe-level license of the application may say nothing about it. Recipe matching cannot detect it. Only binary analysis or a build-system audit that inspects link maps would surface it.
  • Declared license drift. The gate trusts the recipe. If the recipe’s LICENSE is wrong or stale relative to the actual source, the gate makes the right decision on the wrong input. LIC_FILES_CHKSUM helps here: the documentation states the checksums are validated at the end of the configure step and a mismatch fails the build. That catches license text changes in tracked files, but it does not prove the declared expression covers every file in the source tree.

So a stress test that produces a non-compliant image is not evidence that the variable is broken. It is evidence that the variable was never the last line of defense. The gate is a guardrail against accidental inclusion of a package whose recipe declares an unwanted license. It is not a binary auditor.

What the manifest and archiver do — and do not do

Yocto does generate audit aids. The development manual states that a license manifest is generated during image creation under ${DEPLOY_DIR}/licenses/${SSTATE_PKGARCH}/<image-name>-<machine>.rootfs-<datestamp>/ to assist with audits. The archiver class, when inherited with ARCHIVER_MODE[src] = "original", places source from recipes that deploy packages to the image into subdirectories of DEPLOY_DIR/sources based on each recipe’s LICENSE.

Both are records of what the build system believes it shipped. The manifest lists packages the build resolved into the image; the archiver collects source by license category. Neither independently verifies that the binary on the device matches the metadata. If a file entered the rootfs outside the package-deploy path, the manifest will not list it. If a statically linked library is inside a binary, the manifest will not name it. The documentation’s own framing is that these outputs “assist with any audits” — assistance, not attestation.

A forensic verification workflow

The practical response is to treat the build outputs as claims to be checked against the shipped artifact. A workable sequence:

  1. Extract the firmware from the device. Read the SPI flash directly where possible, or use the vendor’s update image. Record the extraction method and hashes so the artifact under audit is fixed.
  2. Identify copyleft binaries and libraries in the extracted rootfs. Look for known GPL/LGPL components, BusyBox-style multi-call binaries, kernel modules, and shared libraries with copyleft provenance. Strings, symbol tables, and build-id metadata are starting points; they are not proof, so flag uncertainty explicitly.
  3. Compare against the license manifest. Every copyleft component you find should have a corresponding entry. A component present in the binary but absent from the manifest is a mismatch worth escalating, whether it arrived through a postprocess step or through a mis-declared recipe.
  4. Check corresponding source availability. For each copyleft component, confirm that the source and build scripts the license requires are actually obtainable, not merely that a manifest entry exists. The archiver output, if used, is a starting point for this check.
  5. Flag mismatches and unresolved items. Where you cannot confirm provenance — for example, a stripped binary with no manifest entry — say so. An unresolved item is a finding; smoothing it over is how compliance failures in this niche begin.

The practical rule

INCOMPATIBLE_LICENSE is worth setting and worth understanding. The 6.1 SPDX changes make its matching more faithful to modern license expressions, and the WITH-expression handling closes a real gap for GPLv3-with-exception cases. But it is a parse-time filter over declared metadata. It cannot see a binary copied into the rootfs, a statically linked copyleft library, or a recipe whose LICENSE no longer describes its source. Before shipping, the question is not whether the build passed the gate. The question is what is actually in the flash, and whether the source and notices that the licenses require are actually available.

FAQ

Does INCOMPATIBLE_LICENSE inspect binaries?

No. In the implementation captured in the public poky commit, it evaluates the recipe’s declared LICENSE during parsing and raises SkipPackage on a match. It does not open binaries or inspect the rootfs.

Can a dual-licensed package still get in?

Yes, if the license expression offers an acceptable branch. The commit’s choose_licenses() logic selects the first operand only when every license in it is acceptable, otherwise the second. The gate follows the declared expression.

What changed in Yocto 6.1?

LICENSE became an SPDX License Expression. INCOMPATIBLE_LICENSE now matches the left-hand side of a WITH expression, so GPLv3-with-exception cases that previously slipped through now match. Allowing a specific exception requires listing it in INCOMPATIBLE_LICENSE_EXCEPTIONS.

Is the license manifest enough for compliance?

No. The documentation describes it as an aid to audits. It records what the build resolved into the image; it does not verify that the shipped binary matches the metadata or that corresponding source is available.

Where can I read the matching logic?

The public poky commit at github.com/YoeDistro/poky shows the incompatible_license() function and its fnmatch-based matching. The Yocto 6.1 migration notes at docs.yoctoproject.org document the SPDX expression change, and the development manual’s licensing chapter at docs.yoctoproject.org/dev-manual/licenses.html covers the manifest and archiver.