docs(cra): add SBOM for CRA complience (#10352)

This commit is contained in:
Gabor Kiss-Vamosi
2026-08-03 10:16:31 +02:00
committed by GitHub
parent 2556491ff2
commit f7ae316b8a
13 changed files with 2334 additions and 19 deletions
+32 -2
View File
@@ -29,7 +29,9 @@ jobs:
python-version: 3.12
- name: Install dependencies
run: python3 -m pip install kconfiglib pytest
# sbom/requirements.txt pins the SBOM validator so an unreviewed
# upstream release cannot silently change or break the compliance gate.
run: python3 -m pip install kconfiglib pytest -r sbom/requirements.txt
# ------------------------------------------------------------------
# Unit tests for the config-headers generator itself.
@@ -57,6 +59,28 @@ jobs:
include/lvgl/config/lv_conf_kconfig.h \
|| (echo "::error::Generated config headers are out of date. Run scripts/generators/config_headers.py and commit the result."; exit 1)
# ------------------------------------------------------------------
# COPYRIGHTS.md and the SPDX SBOM (sbom/) are generated from
# sbom/third_party.json. These fail if a committed output is stale.
# ------------------------------------------------------------------
- name: "Check: COPYRIGHTS.md is up to date"
id: copyrights
continue-on-error: true
run: python3 scripts/generate_copyrights.py --check
- name: "Check: SPDX SBOM is up to date"
id: sbom
continue-on-error: true
run: python3 scripts/generate_sbom.py --check
# ------------------------------------------------------------------
# The SBOM validates against the official SPDX 3.0.1 JSON Schema.
# ------------------------------------------------------------------
- name: "Check: SPDX SBOM is schema-valid"
id: sbom_schema
continue-on-error: true
run: python3 scripts/validate_sbom.py
# ------------------------------------------------------------------
# Final gate fail the job if any generator above is out of date.
# This gives us full output from every generator before failing.
@@ -65,7 +89,10 @@ jobs:
- name: "Gate: fail if any generator check failed"
if: |
steps.config-headers-tests.outcome == 'failure' ||
steps.config-headers.outcome == 'failure'
steps.config-headers.outcome == 'failure' ||
steps.copyrights.outcome == 'failure' ||
steps.sbom.outcome == 'failure' ||
steps.sbom_schema.outcome == 'failure'
run: |
echo "Code generation results:"
echo "------------------------"
@@ -81,6 +108,9 @@ jobs:
print_result "config-headers-tests" "${{ steps.config-headers-tests.outcome }}"
print_result "config-headers" "${{ steps.config-headers.outcome }}"
print_result "copyrights" "${{ steps.copyrights.outcome }}"
print_result "sbom" "${{ steps.sbom.outcome }}"
print_result "sbom_schema" "${{ steps.sbom_schema.outcome }}"
echo "------------------------"
echo "A generator test failed, or a generated file is out of date. See above for details."
+3 -3
View File
@@ -9,7 +9,7 @@ on:
# https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#concurrency
# Ensure that only one commit will be running tests at a time on each PR
concurrency:
group: ${{ github.ref }}-${{ github.workflow }}
group: ${{ github.ref }}-${{ github.workflow }}
cancel-in-progress: true
jobs:
@@ -42,7 +42,7 @@ jobs:
# ------------------------------------------------------------------
# - All #includes inside must be valid relative paths.
# - Angle-bracket includes must be in the explicit allow-list
# - see `ALLOWED_EXTERNAL_HEADERS` in `scripts/static_checks/check_headers.py`
# - see `ALLOWED_EXTERNAL_HEADERS` in `scripts/static_checks/check_headers.py`
# - forbidden headers (stdint.h etc.) must use LV_*_INCLUDE macros.
# ------------------------------------------------------------------
- name: "Check: include path validity + angle-bracket policy"
@@ -67,7 +67,7 @@ jobs:
run: python scripts/static_checks/check_headers.py deprecated
# ------------------------------------------------------------------
# Every header file inside `src` must use `lvgl_public.h` to access
# Every header file inside `src` must use `lvgl_public.h` to access
# the public API
# ------------------------------------------------------------------
- name: "Check: no direct public include from source files"
+35 -14
View File
@@ -1,67 +1,88 @@
LVGL uses the following third-party libraries.
For the licenses, see the corresponding `LICENSE.txt` file in each librarys folder.
Each entry lists its SPDX license expression below. The full license text is, for most libraries, found in the corresponding library folder (see the paths); some libraries only reference their license rather than shipping the full text. A machine-readable inventory is available in `sbom/`.
**Barcode (Barcode generator)**
- Path: src/libs/barcode
- Source: https://github.com/fhunleth/code128
**Expat (XML parser)**
- Path: src/libs/expat
- Source: https://github.com/libexpat/libexpat
- License: BSD-2-Clause
**FreeType (Font rendering library)**
- Path: src/libs/freetype
- Source: https://github.com/freetype/freetype
- License: FTL OR GPL-2.0-or-later
- Note: Only the interfaces are used; FreeType itself is not part of LVGL.
**Liberation Sans (default demo font)**
- Path: src/libs/freetype/LiberationSans-Regular.ttf
- Source: https://github.com/liberationfonts/liberation-fonts
- License: OFL-1.1
- Note: Font asset bundled alongside the FreeType interface; licensed separately from FreeType.
**LodePNG (PNG decoder)**
- Path: src/libs/lodepng
- Source: https://github.com/lvandeve/lodepng
- License: Zlib
**LZ4 (Compression/Decompression)**
- Path: src/libs/lz4
- Source: https://github.com/lz4/lz4
- License: BSD-2-Clause
**QR Code (QR code generator)**
- Path: src/libs/qrcode
- Source: https://github.com/nayuki/QR-Code-generator
- License: MIT
**ThorVG (Vector graphics rendering)**
- Path: src/libs/thorvg
- Source: https://github.com/thorvg/thorvg
- License: MIT
**RapidJSON (JSON parser/generator)**
- Path: src/libs/thorvg/rapidjson
- Source: https://github.com/Tencent/rapidjson
- License: MIT AND BSD-3-Clause
- Note: Bundled inside the ThorVG tree (used by ThorVG's Lottie parser); licensed separately from ThorVG. The bundled msinttypes helper (rapidjson/msinttypes) is BSD-3-Clause (Copyright (c) 2006-2013 Alexander Chemeris), hence the combined license expression.
**TinyTTF**
- Path: src/libs/tiny_ttf
- Source:
- https://github.com/nothings/stb (*Only parts are integrated*)
- https://github.com/codewitch-honey-crisis/tiny_ttf (*Modified version of the original STB library*)
- https://github.com/nothings/stb (*Only parts are integrated*)
- https://github.com/codewitch-honey-crisis/tiny_ttf (*Modified version of the original STB library*)
- License: MIT OR Unlicense
**TJPGD (JPEG decoder)**
- Path: src/libs/tjpgd
- Source: http://elm-chan.org/fsw/tjpgd/00index.html
- License: LicenseRef-TJpgDec
**TLSF (Two-Level Segregate Fit memory allocator)**
- Path: src/stdlib/builtin
- Source: https://github.com/mattconte/tlsf
- License: BSD-3-Clause
**Printf (Printf formatting library)**
- Path: src/stdlib/builtin
- Source: https://github.com/mpaland/printf
**LVGL's XML format**
- Path:
- docs/src/auxiliary-modules/xml
- src/others/xml
- xmls
- License: MIT
**FT800-FT813 (EVE GPU driver)**
- Path src/libs/FT800-FT813
- Path: src/libs/FT800-FT813
- Source: https://github.com/RudolphRiedel/FT800-FT813
- License: MIT
**AnimatedGIF (GIF decoder library)**
- Path: src/libs/gif
- Source: https://github.com/bitbank2/AnimatedGIF
- License: Apache-2.0
**NanoVG (Anti-aliased vector graphics rendering)**
- Path: src/libs/nanovg
- Source: https://github.com/memononen/nanovg
- License: Zlib
- Note: Compiled when LV_USE_NANOVG (or the LV_USE_DRAW_NANOVG backend) is enabled.
**FrogFS (read-only filesystem)**
- Path: src/libs/frogfs
- Source: https://github.com/jkent/frogfs
- License: MPL-2.0
+35
View File
@@ -0,0 +1,35 @@
# Security Policy
## Reporting a vulnerability
**Please do not open a public GitHub issue for security problems.**
Report suspected vulnerabilities privately by email to
**security@lvgl.io**.
Please include, as far as you can:
- the affected version(s) and configuration (`lv_conf.h` toggles, target),
- a description of the impact,
- steps to reproduce or a proof of concept, and
- any suggested fix or mitigation.
We support coordinated disclosure: please give us a reasonable window to
release a fix before any public disclosure. We will credit you in the advisory
unless you prefer otherwise.
## Supported versions
Fixes are made available for the actively supported releases. See the
[Policies page](https://docs.lvgl.io/master/introduction/policies) for the
current support table.
## SBOM and more
LVGL publishes a machine-readable SBOM (SPDX 3.0.1) in the [`sbom/`](sbom/)
folder and a human-readable component list in
[`COPYRIGHTS.md`](COPYRIGHTS.md).
For our full security commitment, vulnerability handling process, and how LVGL
supports users' EU Cyber Resilience Act (CRA) obligations, see the
[Security page in the documentation](https://docs.lvgl.io/master/introduction/security).
+1
View File
@@ -5,6 +5,7 @@
"requirements",
"license",
"policies",
"security",
"faq"
]
}
+100
View File
@@ -0,0 +1,100 @@
---
title: Security
description: "LVGL's security commitment, vulnerability reporting process, SBOM, and how LVGL supports our users' EU Cyber Resilience Act (CRA) obligations"
---
## Our commitment
LVGL is used in millions of devices, many of them shipped commercially and
placed on regulated markets. We take the security of the library and of the
products built on it seriously. LVGL Kft., as the legal entity stewarding the
project, maintains a cybersecurity policy that covers:
- a documented **coordinated vulnerability disclosure** process,
- a published, machine-readable **Software Bill of Materials (SBOM)**,
- vulnerability handling with public security advisories, and
- cooperation with users, integrators, and the relevant authorities.
This page describes those processes and how they help you meet your own
obligations under regulations such as the EU Cyber Resilience Act (CRA).
## Reporting a vulnerability
**Please do not open a public GitHub issue for security problems.**
Report suspected vulnerabilities privately by email to
[security@lvgl.io](mailto:security@lvgl.io).
Please include, as far as you can:
- the affected version(s) and configuration (`lv_conf.h` toggles, target),
- a description of the impact,
- steps to reproduce or a proof of concept, and
- any suggested fix or mitigation.
We support **coordinated disclosure**: we ask that you give us a reasonable
window to release a fix before any public disclosure, and we will credit you in
the advisory unless you prefer otherwise.
## How we handle reports
| Stage | What happens |
| --- | --- |
| Acknowledge | We confirm receipt of your report, typically within a few business days. |
| Triage | We reproduce and assess severity, and determine the affected versions. |
| Fix | We develop and review a fix on a private track when needed. |
| Advisory | We publish a [GitHub Security Advisory](https://github.com/lvgl/lvgl/security/advisories) and, where applicable, request a CVE identifier. |
| Release | The fix ships in a patch release; see [Policies](/introduction/policies) for the supported versions. |
Fixes are made available for the actively supported releases listed on the
[Policies](/introduction/policies) page.
## Software Bill of Materials (SBOM)
LVGL publishes two views of its component inventory:
- [**`sbom/lvgl.spdx.json`**](https://github.com/lvgl/lvgl/blob/master/sbom/lvgl.spdx.json): the **machine-readable** SBOM, in the
[SPDX 3.0.1](https://spdx.github.io/spdx-spec/v3.0.1/) JSON-LD format, a
commonly used standard suitable for automated tooling.
- [**`COPYRIGHTS.md`**](https://github.com/lvgl/lvgl/blob/master/COPYRIGHTS.md): the **human-readable** list of every third-party component with its license
and copyright.
Both are generated from a single source of truth,
[`sbom/third_party.json`](https://github.com/lvgl/lvgl/blob/master/sbom/third_party.json),
and their correctness is enforced by CI, so they always track the checked-out
version.
See the [SBOM README](https://github.com/lvgl/lvgl/blob/master/sbom/README.md)
for the file layout, how to regenerate the artifacts, and how to validate them.
Note that some optional dependencies (for example FreeType) are **provided by
the integrator** rather than bundled with LVGL; the SBOM lists LVGL's
integration interface, but you are responsible for the SBOM entries of the
components you supply yourself.
## LVGL and the EU Cyber Resilience Act (CRA)
The CRA (Regulation (EU) 2024/2847) places obligations on **manufacturers**
who place *products with digital elements* on the EU market. LVGL is a free and
open-source **software component**, not a finished product placed on the market
by LVGL Kft. The CRA obligations for a shipped product therefore fall on the
**manufacturer that integrates LVGL** into that product.
What LVGL provides is the upstream foundation that makes meeting those
obligations easier:
- a **machine-readable SBOM** covering LVGL and its bundled third-party
components,
- a **coordinated vulnerability disclosure** process and public security
advisories, and
- clear licensing and component provenance in `COPYRIGHTS.md`.
As the open-source steward of the project, LVGL Kft. maintains the cybersecurity
policy described on this page and cooperates with users and authorities on
vulnerability handling.
> **Not legal advice.** This page describes LVGL's processes and artifacts. It
> is not legal advice, and it does not by itself make any product compliant. The
> precise classification of your product and of LVGL Kft. under the CRA, and the
> steps required for your conformity, should be confirmed with qualified
> counsel. For questions, contact [lvgl@lvgl.io](mailto:lvgl@lvgl.io).
+86
View File
@@ -0,0 +1,86 @@
# LVGL SBOM
This folder contains the Software Bill of Materials (SBOM) for LVGL in
[SPDX 3.0.1](https://spdx.github.io/spdx-spec/v3.0.1/) JSON-LD format, plus the
single source of truth it is generated from.
## Files
| File | Description |
|------|-------------|
| `third_party.json` | **Single source of truth** — every third-party dependency, its license, supplier and source |
| `lvgl.spdx.json` | Generated SPDX 3.0.1 SBOM |
| `requirements.txt` | Pinned tooling for validation / the CI gate |
| `README.md` | This file |
Two artifacts are **generated** from `third_party.json` — do not edit them by hand:
- `sbom/lvgl.spdx.json` (this folder)
- `COPYRIGHTS.md` (repo root)
## Regenerating
When a dependency is added, removed, or relicensed, edit
`sbom/third_party.json` and regenerate both outputs:
```sh
python3 scripts/generate_copyrights.py # -> COPYRIGHTS.md
python3 scripts/generate_sbom.py # -> sbom/lvgl.spdx.json
```
The LVGL version is read from `include/lvgl/lv_version.h` and recorded inside
the document, so the package version always tracks the checked-out tree. The
file name itself is unversioned — the SBOM is bundled inside a given LVGL
version anyway.
## Validating
The SBOM is validated against the **official SPDX 3.0.1 JSON Schema** (SPDX's
own recommended method) using
[check-jsonschema](https://github.com/python-jsonschema/check-jsonschema):
```sh
python3 -m pip install -r sbom/requirements.txt
python3 scripts/validate_sbom.py
```
## CI
The `Static Checks` workflow (`.github/workflows/static_checks.yml`) fails the
build if any generated file drifts from `sbom/third_party.json` or the SBOM is
not schema-valid:
- `generate_copyrights.py --check` — COPYRIGHTS.md is up to date
- `generate_sbom.py --check` — SBOM is up to date (ignores the creation timestamp)
- `validate_sbom.py` — SBOM validates against the SPDX JSON Schema
## `third_party.json` fields
Each entry in `components`:
| field | used by | notes |
|---------------|--------------------|-------|
| `key` | SBOM | slug for SPDX element ids |
| `name` | SBOM + COPYRIGHTS | component title |
| `paths` | SBOM + COPYRIGHTS | in-tree locations |
| `sources` | SBOM + COPYRIGHTS | `[{url, note?}]`; array because e.g. TinyTTF has two upstreams. First url is the SPDX download location |
| `note` | SBOM + COPYRIGHTS | free-form note |
| `version` | SBOM | vendored snapshot version (from an in-tree version macro/string), or `NOASSERTION` when the upstream copy carries no version |
| `license` | SBOM | SPDX license expression (or `LicenseRef-*`) |
| `license_file`| SBOM | path to embed for a custom `LicenseRef-*` license |
| `copyright` | SBOM | upstream copyright statement (from the library's LICENSE/source header) |
| `supplier` | SBOM | `{name, type}` where type is `person` or `organization` |
| `purl` | SBOM | Package URL identifier |
| `purpose` | SBOM | SPDX `software_primaryPurpose` |
| `third_party` | SBOM | `false` marks LVGL's own code — omitted from the SBOM component list (COPYRIGHTS.md renders every entry) |
## Notes on the data
- The inventory was curated from `COPYRIGHTS.md`, the `LV_USE_*` toggles in
`lv_conf_template.h`, and the `src/libs/**/LICENSE*` files.
- **FreeType** is listed because LVGL ships its integration interface, but
FreeType itself is not part of LVGL and must be provided by the integrator.
- In SPDX 3.0 a package has no license field; licensing is expressed as
`Relationship` elements (`from` package → `hasDeclaredLicense` /
`hasConcludedLicense``to` license expression). This is why the SBOM
contains `Relationship` nodes.
+1271
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
# Tooling for the SBOM compliance gate (see .github/workflows/code_generation.yml).
# The validator version is pinned so the gate does not change or break from an
# unreviewed upstream release; bump it here deliberately when updating.
check-jsonschema==0.37.4
+214
View File
@@ -0,0 +1,214 @@
{
"$comment": "Single source of truth for LVGL's third-party dependencies. Edit this file when a dependency is added, removed, or relicensed, then run scripts/generate_copyrights.py and scripts/generate_sbom.py to regenerate COPYRIGHTS.md and the SPDX SBOM. 'supplier' names come from each library's copyright holder. 'version' is the vendored snapshot version (from an in-tree version macro/string) or NOASSERTION when the upstream copy carries no version. 'copyright' is the upstream copyright statement from the library's LICENSE/source header.",
"project": {
"name": "lvgl",
"summary": "Light and Versatile Graphics Library",
"license": "MIT",
"copyright": "Copyright (c) 2025 LVGL Kft",
"download_location": "https://github.com/lvgl/lvgl",
"homepage": "https://lvgl.io",
"supplier": {"name": "LVGL Kft", "type": "organization"}
},
"components": [
{
"key": "barcode",
"name": "Barcode (Barcode generator)",
"paths": ["src/libs/barcode"],
"sources": [{"url": "https://github.com/fhunleth/code128"}],
"version": "NOASSERTION",
"license": "BSD-2-Clause",
"copyright": "Copyright (c) 2013-2015 LKC Technologies, Inc.",
"purl": "pkg:github/fhunleth/code128",
"purpose": "library",
"supplier": {"name": "LKC Technologies, Inc.", "type": "organization"}
},
{
"key": "freetype",
"name": "FreeType (Font rendering library)",
"paths": ["src/libs/freetype"],
"sources": [{"url": "https://github.com/freetype/freetype"}],
"note": "Only the interfaces are used; FreeType itself is not part of LVGL.",
"version": "NOASSERTION",
"license": "FTL OR GPL-2.0-or-later",
"copyright": "Copyright (C) 1996-2022 by David Turner, Robert Wilhelm, and Werner Lemberg (The FreeType Project)",
"purl": "pkg:github/freetype/freetype",
"purpose": "library",
"supplier": {"name": "The FreeType Project", "type": "organization"}
},
{
"key": "liberation-sans",
"name": "Liberation Sans (default demo font)",
"paths": ["src/libs/freetype/LiberationSans-Regular.ttf"],
"sources": [{"url": "https://github.com/liberationfonts/liberation-fonts"}],
"note": "Font asset bundled alongside the FreeType interface; licensed separately from FreeType.",
"version": "NOASSERTION",
"license": "OFL-1.1",
"copyright": "Digitized data copyright (c) 2010 Google Corporation with Reserved Font Arimo, Tinos and Cousine; Copyright (c) 2012 Red Hat, Inc. with Reserved Font Name Liberation.",
"purl": "pkg:github/liberationfonts/liberation-fonts",
"purpose": "file",
"supplier": {"name": "Red Hat, Inc.", "type": "organization"}
},
{
"key": "lodepng",
"name": "LodePNG (PNG decoder)",
"paths": ["src/libs/lodepng"],
"sources": [{"url": "https://github.com/lvandeve/lodepng"}],
"version": "20230410",
"license": "Zlib",
"copyright": "Copyright (c) 2005-2023 Lode Vandevenne",
"purl": "pkg:github/lvandeve/lodepng",
"purpose": "library",
"supplier": {"name": "Lode Vandevenne", "type": "person"}
},
{
"key": "lz4",
"name": "LZ4 (Compression/Decompression)",
"paths": ["src/libs/lz4"],
"sources": [{"url": "https://github.com/lz4/lz4"}],
"version": "1.10.0",
"license": "BSD-2-Clause",
"copyright": "Copyright (c) 2011-2023 Yann Collet",
"purl": "pkg:github/lz4/lz4",
"purpose": "library",
"supplier": {"name": "Yann Collet", "type": "person"}
},
{
"key": "qrcode",
"name": "QR Code (QR code generator)",
"paths": ["src/libs/qrcode"],
"sources": [{"url": "https://github.com/nayuki/QR-Code-generator"}],
"version": "NOASSERTION",
"license": "MIT",
"copyright": "Copyright (c) Project Nayuki. (MIT License)",
"purl": "pkg:github/nayuki/QR-Code-generator",
"purpose": "library",
"supplier": {"name": "Project Nayuki", "type": "organization"}
},
{
"key": "thorvg",
"name": "ThorVG (Vector graphics rendering)",
"paths": ["src/libs/thorvg"],
"sources": [{"url": "https://github.com/thorvg/thorvg"}],
"version": "0.15.3",
"license": "MIT",
"copyright": "Copyright (c) 2020 - 2025 notice for the ThorVG Project (see CONTRIBUTORS)",
"purl": "pkg:github/thorvg/thorvg",
"purpose": "library",
"supplier": {"name": "The ThorVG Project", "type": "organization"}
},
{
"key": "rapidjson",
"name": "RapidJSON (JSON parser/generator)",
"paths": ["src/libs/thorvg/rapidjson"],
"sources": [{"url": "https://github.com/Tencent/rapidjson"}],
"note": "Bundled inside the ThorVG tree (used by ThorVG's Lottie parser); licensed separately from ThorVG. The bundled msinttypes helper (rapidjson/msinttypes) is BSD-3-Clause (Copyright (c) 2006-2013 Alexander Chemeris), hence the combined license expression.",
"version": "1.1.0",
"license": "MIT AND BSD-3-Clause",
"copyright": "Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip.; Copyright (c) 2006-2013 Alexander Chemeris (msinttypes)",
"purl": "pkg:github/Tencent/rapidjson@1.1.0",
"purpose": "library",
"supplier": {"name": "THL A29 Limited, a Tencent company", "type": "organization"}
},
{
"key": "tiny_ttf",
"name": "TinyTTF",
"paths": ["src/libs/tiny_ttf"],
"sources": [
{"url": "https://github.com/nothings/stb", "note": "Only parts are integrated"},
{"url": "https://github.com/codewitch-honey-crisis/tiny_ttf", "note": "Modified version of the original STB library"}
],
"version": "1.26",
"license": "MIT OR Unlicense",
"copyright": "Copyright (c) 2017 Sean Barrett; Copyright (c) 2022 honey the codewitch",
"purl": "pkg:github/nothings/stb",
"purpose": "library",
"supplier": {"name": "Sean Barrett", "type": "person"}
},
{
"key": "tjpgd",
"name": "TJPGD (JPEG decoder)",
"paths": ["src/libs/tjpgd"],
"sources": [{"url": "http://elm-chan.org/fsw/tjpgd/00index.html"}],
"version": "R0.03",
"license": "LicenseRef-TJpgDec",
"license_file": "src/libs/tjpgd/LICENSE.txt",
"copyright": "Copyright (C) 2021, ChaN, all right reserved.",
"purl": "pkg:generic/tjpgd?download_url=http://elm-chan.org/fsw/tjpgd/00index.html",
"purpose": "library",
"supplier": {"name": "ChaN", "type": "person"}
},
{
"key": "tlsf",
"name": "TLSF (Two-Level Segregate Fit memory allocator)",
"paths": ["src/stdlib/builtin"],
"sources": [{"url": "https://github.com/mattconte/tlsf"}],
"version": "3.1",
"license": "BSD-3-Clause",
"copyright": "Copyright (c) 2006-2016 Matthew Conte",
"purl": "pkg:github/mattconte/tlsf",
"purpose": "library",
"supplier": {"name": "Matthew Conte", "type": "person"}
},
{
"key": "printf",
"name": "Printf (Printf formatting library)",
"paths": ["src/stdlib/builtin"],
"sources": [{"url": "https://github.com/mpaland/printf"}],
"version": "NOASSERTION",
"license": "MIT",
"copyright": "Copyright (c) 2014 Marco Paland",
"purl": "pkg:github/mpaland/printf",
"purpose": "library",
"supplier": {"name": "Marco Paland", "type": "person"}
},
{
"key": "ft800-ft813",
"name": "FT800-FT813 (EVE GPU driver)",
"paths": ["src/libs/FT800-FT813"],
"sources": [{"url": "https://github.com/RudolphRiedel/FT800-FT813"}],
"version": "5.0",
"license": "MIT",
"copyright": "Copyright (c) 2016-2024 Rudolph Riedel",
"purl": "pkg:github/RudolphRiedel/FT800-FT813",
"purpose": "library",
"supplier": {"name": "Rudolph Riedel", "type": "person"}
},
{
"key": "gif",
"name": "AnimatedGIF (GIF decoder library)",
"paths": ["src/libs/gif"],
"sources": [{"url": "https://github.com/bitbank2/AnimatedGIF"}],
"version": "NOASSERTION",
"license": "Apache-2.0",
"copyright": "Copyright (c) 2020 BitBank Software, Inc. All rights reserved.",
"purl": "pkg:github/bitbank2/AnimatedGIF",
"purpose": "library",
"supplier": {"name": "BitBank Software, Inc.", "type": "organization"}
},
{
"key": "nanovg",
"name": "NanoVG (Anti-aliased vector graphics rendering)",
"paths": ["src/libs/nanovg"],
"sources": [{"url": "https://github.com/memononen/nanovg"}],
"note": "Compiled when LV_USE_NANOVG (or the LV_USE_DRAW_NANOVG backend) is enabled.",
"version": "NOASSERTION",
"license": "Zlib",
"copyright": "Copyright (c) 2013 Mikko Mononen memon@inside.org",
"purl": "pkg:github/memononen/nanovg",
"purpose": "library",
"supplier": {"name": "Mikko Mononen", "type": "person"}
},
{
"key": "frogfs",
"name": "FrogFS (read-only filesystem)",
"paths": ["src/libs/frogfs"],
"sources": [{"url": "https://github.com/jkent/frogfs"}],
"version": "1.0",
"license": "MPL-2.0",
"copyright": "Copyright (c) Jeff Kent",
"purl": "pkg:github/jkent/frogfs",
"purpose": "library",
"supplier": {"name": "Jeff Kent", "type": "person"}
}
]
}
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Regenerate COPYRIGHTS.md from the single source of truth.
Component data lives in sbom/third_party.json, which is also used to
generate the SPDX SBOM (see generate_sbom.py). Edit that JSON, then run this
script to refresh COPYRIGHTS.md.
Usage:
python3 scripts/generate_copyrights.py [--output COPYRIGHTS.md] [--check]
--check exit non-zero if COPYRIGHTS.md is out of date (for CI).
"""
import argparse
import json
import os
import sys
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATA_FILE = os.path.join(REPO_ROOT, "sbom", "third_party.json")
HEADER = (
"LVGL uses the following third-party libraries.\n"
"Each entry lists its SPDX license expression below. The full license text "
"is, for most libraries, found in the corresponding library folder (see the "
"paths); some libraries only reference their license rather than shipping "
"the full text. A machine-readable inventory is available in `sbom/`.\n"
)
def render(data):
lines = [HEADER]
for comp in data["components"]:
lines.append("**%s**" % comp["name"])
paths = comp.get("paths", [])
if len(paths) == 1:
lines.append("- Path: %s" % paths[0])
elif paths:
lines.append("- Path:")
for p in paths:
lines.append(" - %s" % p)
sources = comp.get("sources", [])
if len(sources) == 1 and not sources[0].get("note"):
lines.append("- Source: %s" % sources[0]["url"])
elif sources:
lines.append("- Source:")
for s in sources:
suffix = " (*%s*)" % s["note"] if s.get("note") else ""
lines.append(" - %s%s" % (s["url"], suffix))
if comp.get("license"):
lines.append("- License: %s" % comp["license"])
if comp.get("note"):
lines.append("- Note: %s" % comp["note"])
lines.append("") # blank line between entries
return "\n".join(lines).rstrip() + "\n"
def main():
parser = argparse.ArgumentParser(description="Regenerate COPYRIGHTS.md.")
parser.add_argument("--output", default=os.path.join(REPO_ROOT, "COPYRIGHTS.md"),
help="Output file (default: COPYRIGHTS.md).")
parser.add_argument("--check", action="store_true",
help="Exit non-zero if the output is out of date.")
args = parser.parse_args()
with open(DATA_FILE, encoding="utf-8") as fh:
data = json.load(fh)
content = render(data)
if args.check:
current = ""
if os.path.isfile(args.output):
with open(args.output, encoding="utf-8") as fh:
current = fh.read()
if current != content:
print("%s is out of date; run scripts/generate_copyrights.py" % args.output)
return 1
print("%s is up to date" % args.output)
return 0
with open(args.output, "w", encoding="utf-8") as fh:
fh.write(content)
print("Wrote %s (%d components)" % (args.output, len(data["components"])))
return 0
if __name__ == "__main__":
sys.exit(main())
+392
View File
@@ -0,0 +1,392 @@
#!/usr/bin/env python3
"""Generate a Software Bill of Materials (SBOM) for LVGL in SPDX 3.0 (JSON-LD).
The component data comes from a single source of truth:
sbom/third_party.json
which is also used to regenerate COPYRIGHTS.md (see generate_copyrights.py).
The LVGL version is read at run time from include/lvgl/lv_version.h so the
document always matches the checked-out tree.
Output: SPDX 3.0.1 JSON-LD written to the SBOM folder (default: ./sbom).
Usage:
python3 scripts/generate_sbom.py [--output-dir sbom] [--date 2026-07-15]
SPDX 3.0.1 spec: https://spdx.github.io/spdx-spec/v3.0.1/
"""
import argparse
import datetime
import json
import os
import re
import sys
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATA_FILE = os.path.join(REPO_ROOT, "sbom", "third_party.json")
# Namespace used to mint SPDX element identifiers (URIs). The version is
# appended at run time so different releases produce distinct id spaces.
NS_BASE = "https://lvgl.io/spdx"
def read_version():
"""Return the LVGL version string, e.g. '9.6.0-dev'."""
path = os.path.join(REPO_ROOT, "include", "lvgl", "lv_version.h")
with open(path, encoding="utf-8") as fh:
text = fh.read()
def grab(macro):
m = re.search(r"#define\s+%s\s+\"?([^\"\n]+)\"?" % macro, text)
return m.group(1).strip() if m else None
version = "%s.%s.%s" % (grab("LVGL_VERSION_MAJOR"),
grab("LVGL_VERSION_MINOR"),
grab("LVGL_VERSION_PATCH"))
info = grab("LVGL_VERSION_INFO")
if info:
version += "-" + info
return version
def read_file(rel_path):
"""Return the text of a repo file, or None if it is not present."""
path = os.path.join(REPO_ROOT, rel_path)
if not os.path.isfile(path):
return None
with open(path, encoding="utf-8", errors="replace") as fh:
return fh.read()
def build_document(data, version, spec_version, created):
"""Build the SPDX 3.0 JSON-LD document as a Python dict."""
project = data["project"]
# Third-party components only (LVGL's own code is part of the root package).
components = [c for c in data["components"] if c.get("third_party", True)]
ns = "%s/lvgl-%s" % (NS_BASE, version)
def sid(fragment):
return "%s#%s" % (ns, fragment)
creation_info = "_:creationinfo"
tool_id = sid("Tool-generate_sbom.py")
doc_id = sid("SPDXDocument")
sbom_id = sid("SBOM")
root_id = sid("Package-%s" % project["name"])
graph = []
# --- Agent registry ----------------------------------------------------
# Suppliers/creators (Person/Organization) are deduplicated by name and
# emitted once at the end of the graph.
agents = {} # name -> {"id": ..., "type": ...}
def agent_ref(supplier):
"""Register a supplier {name, type} and return its SPDX element id."""
name = supplier["name"]
if name not in agents:
slug = re.sub(r"[^A-Za-z0-9]+", "-", name).strip("-")
kind = "Organization" if supplier.get("type") == "organization" else "Person"
agents[name] = {"id": sid("Agent-" + slug), "type": kind}
return agents[name]["id"]
creator_id = agent_ref({"name": "LVGL Kft", "type": "organization"})
# --- CreationInfo (blank node) -----------------------------------------
graph.append({
"type": "CreationInfo",
"@id": creation_info,
"specVersion": spec_version,
"created": created,
"createdBy": [creator_id],
"createdUsing": [tool_id],
})
graph.append({
"type": "Tool",
"spdxId": tool_id,
"creationInfo": creation_info,
"name": "scripts/generate_sbom.py",
})
# --- Licensing elements ------------------------------------------------
# Custom (non-SPDX-list) licenses referenced by a LicenseRef-* need an
# explicit CustomLicense element so the reference resolves in-document.
# Build these first so their URIs can be wired into the LicenseExpression
# elements below via simplelicensing_customIdToUri.
custom_license_uris = {} # "LicenseRef-*" -> CustomLicense element id
for comp in components:
ref = comp["license"]
if not ref.startswith("LicenseRef-"):
continue
if ref in custom_license_uris:
continue
license_file = comp.get("license_file")
if not license_file:
raise SystemExit(
"Custom license %r (component %r) needs a 'license_file' in "
"third_party.json so its text can be embedded." % (ref, comp["key"]))
text = read_file(license_file)
if text is None:
raise SystemExit(
"License file %r for custom license %r (component %r) is "
"missing; cannot embed its text." % (license_file, ref, comp["key"]))
cid = sid("CustomLicense-" + re.sub(r"[^A-Za-z0-9]+", "_", ref))
custom_license_uris[ref] = cid
graph.append({
"type": "expandedlicensing_CustomLicense",
"spdxId": cid,
"creationInfo": creation_info,
"name": ref,
# Schema requires the license text under simplelicensing_licenseText.
"simplelicensing_licenseText": text,
"expandedlicensing_isOsiApproved": False,
})
# One LicenseExpression element per distinct expression; reused by relationships.
all_licenses = sorted({c["license"] for c in components} | {project["license"]})
license_expr_ids = {}
for expr in all_licenses:
frag = "LicenseExpression-" + re.sub(r"[^A-Za-z0-9.]+", "_", expr)
lid = sid(frag)
license_expr_ids[expr] = lid
element = {
"type": "simplelicensing_LicenseExpression",
"spdxId": lid,
"creationInfo": creation_info,
"simplelicensing_licenseExpression": expr,
}
# Map any LicenseRef-* tokens in this expression to their CustomLicense
# element so consumers can resolve the custom license text.
id_to_uri = [
{"type": "DictionaryEntry", "key": ref, "value": uri}
for ref, uri in sorted(custom_license_uris.items())
if re.search(r"\b%s\b" % re.escape(ref), expr)
]
if id_to_uri:
element["simplelicensing_customIdToUri"] = id_to_uri
graph.append(element)
# --- LVGL root package -------------------------------------------------
graph.append({
"type": "software_Package",
"spdxId": root_id,
"creationInfo": creation_info,
"name": project["name"],
"summary": project.get("summary", ""),
"software_packageVersion": version,
"software_copyrightText": project.get("copyright", "NOASSERTION"),
"software_downloadLocation": project["download_location"],
"software_homePage": project.get("homepage", project["download_location"]),
"software_primaryPurpose": "library",
"suppliedBy": agent_ref(project["supplier"]),
"externalIdentifier": [{
"type": "ExternalIdentifier",
"externalIdentifierType": "packageUrl",
"identifier": "pkg:github/lvgl/lvgl@%s" % version,
}],
})
# --- Third-party component packages ------------------------------------
component_ids = []
relationships = []
for comp in components:
cid = sid("Package-" + comp["key"])
component_ids.append(cid)
sources = comp.get("sources", [])
source_urls = [s["url"] for s in sources]
comment = "In-tree path: %s" % ", ".join(comp["paths"])
if comp.get("note"):
comment += " | " + comp["note"]
# The first source is the SPDX download location (below); preserve any
# additional upstream sources in the comment so they are not dropped.
for extra in sources[1:]:
suffix = " (%s)" % extra["note"] if extra.get("note") else ""
comment += " | Additional source: %s%s" % (extra["url"], suffix)
pkg = {
"type": "software_Package",
"spdxId": cid,
"creationInfo": creation_info,
"name": comp["name"],
"software_packageVersion": comp.get("version", "NOASSERTION"),
"software_copyrightText": comp.get("copyright", "NOASSERTION"),
"software_primaryPurpose": comp.get("purpose", "library"),
"comment": comment,
}
if comp.get("supplier"):
pkg["suppliedBy"] = agent_ref(comp["supplier"])
if source_urls:
pkg["software_downloadLocation"] = source_urls[0]
pkg["software_homePage"] = source_urls[0]
if comp.get("purl"):
purl = comp["purl"]
# Note: use a distinct name here — do not reuse `version`, which
# holds the LVGL document version used further below.
comp_version = comp.get("version", "NOASSERTION")
# Qualify the purl with the vendored version when one is known and
# the purl does not already carry a version/qualifier of its own.
if comp_version and comp_version != "NOASSERTION" and "@" not in purl and "?" not in purl:
purl = "%s@%s" % (purl, comp_version)
pkg["externalIdentifier"] = [{
"type": "ExternalIdentifier",
"externalIdentifierType": "packageUrl",
"identifier": purl,
}]
graph.append(pkg)
# lvgl vendors (contains) the third-party copy. We deliberately do not
# emit a blanket `dependsOn` here: many of these libraries are optional
# (gated behind LV_USE_* toggles that are off by default), so claiming
# lvgl depends on all of them would overstate the dependency graph.
relationships.append({
"type": "Relationship",
"spdxId": sid("Relationship-contains-%s" % comp["key"]),
"creationInfo": creation_info,
"from": root_id,
"relationshipType": "contains",
"to": [cid],
})
# component license (declared == concluded for a vendored copy)
for rel_type in ("hasDeclaredLicense", "hasConcludedLicense"):
relationships.append({
"type": "Relationship",
"spdxId": sid("Relationship-%s-%s" % (rel_type, comp["key"])),
"creationInfo": creation_info,
"from": cid,
"relationshipType": rel_type,
"to": [license_expr_ids[comp["license"]]],
})
# lvgl's own license
for rel_type in ("hasDeclaredLicense", "hasConcludedLicense"):
relationships.append({
"type": "Relationship",
"spdxId": sid("Relationship-%s-%s" % (rel_type, project["name"])),
"creationInfo": creation_info,
"from": root_id,
"relationshipType": rel_type,
"to": [license_expr_ids[project["license"]]],
})
graph.extend(relationships)
# --- Agent elements (creator + package suppliers) ----------------------
for name, info in agents.items():
graph.append({
"type": info["type"],
"spdxId": info["id"],
"creationInfo": creation_info,
"name": name,
})
# --- SBOM element ------------------------------------------------------
# Every element built so far (packages, licenses, relationships, agents,
# tool) is part of the SBOM's contents, so list them all here. Otherwise a
# consumer starting from the SBOM root cannot reach the relationships.
sbom_elements = [e["spdxId"] for e in graph if e.get("spdxId")]
graph.append({
"type": "software_Sbom",
"spdxId": sbom_id,
"creationInfo": creation_info,
"software_sbomType": ["source"],
"rootElement": [root_id],
"element": sbom_elements,
})
# --- SpdxDocument (wraps everything) -----------------------------------
doc_elements = [e.get("spdxId") for e in graph if e.get("spdxId")]
graph.append({
"type": "SpdxDocument",
"spdxId": doc_id,
"creationInfo": creation_info,
"name": "LVGL %s SBOM" % version,
"profileConformance": ["core", "software", "simpleLicensing", "expandedLicensing"],
"rootElement": [sbom_id],
"element": doc_elements,
})
document = {
"@context": "https://spdx.org/rdf/%s/spdx-context.jsonld" % spec_version,
"@graph": graph,
}
return document
def _serialize(document):
return json.dumps(document, indent=2, ensure_ascii=False) + "\n"
def _normalized(document):
"""Copy with the volatile creation timestamp blanked, for drift comparison."""
doc = json.loads(json.dumps(document))
for element in doc.get("@graph", []):
if element.get("type") == "CreationInfo":
element["created"] = "<normalized>"
return _serialize(doc)
def main():
parser = argparse.ArgumentParser(description="Generate an SPDX 3.0 SBOM for LVGL.")
parser.add_argument("--output-dir", default=os.path.join(REPO_ROOT, "sbom"),
help="Directory to write the SBOM into (default: ./sbom).")
parser.add_argument("--date", default=None,
help="Creation date (YYYY-MM-DD). Defaults to today (UTC).")
parser.add_argument("--spec-version", default="3.0.1",
help="SPDX specification version (default: 3.0.1).")
parser.add_argument("--check", action="store_true",
help="Exit non-zero if the committed SBOM is out of date "
"(ignores the creation timestamp).")
args = parser.parse_args()
with open(DATA_FILE, encoding="utf-8") as fh:
data = json.load(fh)
version = read_version()
# The file name carries no version: the SBOM is bundled inside a given LVGL
# version anyway, and the version is recorded inside the document.
out_path = os.path.join(args.output_dir, "lvgl.spdx.json")
if args.date:
# strptime alone accepts non-zero-padded input like "2026-7-1", which
# would produce an invalid SPDX timestamp, so enforce the exact shape.
if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", args.date):
parser.error("--date must be in YYYY-MM-DD format (zero-padded)")
try:
datetime.datetime.strptime(args.date, "%Y-%m-%d")
except ValueError:
parser.error("--date must be a valid date in YYYY-MM-DD format")
created = "%sT00:00:00Z" % args.date
else:
created = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
document = build_document(data, version, args.spec_version, created)
components = [c for c in data["components"] if c.get("third_party", True)]
if args.check:
if not os.path.isfile(out_path):
print("%s is missing; run scripts/generate_sbom.py" % out_path)
return 1
with open(out_path, encoding="utf-8") as fh:
current = fh.read()
if _normalized(json.loads(current)) != _normalized(document):
print("%s is out of date; run scripts/generate_sbom.py" % out_path)
return 1
print("%s is up to date" % out_path)
return 0
os.makedirs(args.output_dir, exist_ok=True)
with open(out_path, "w", encoding="utf-8") as fh:
fh.write(_serialize(document))
print("Wrote SPDX %s SBOM for LVGL %s" % (args.spec_version, version))
print(" %s" % out_path)
print(" %d third-party components, %d SPDX elements"
% (len(components), len(document["@graph"])))
return 0
if __name__ == "__main__":
sys.exit(main())
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Validate LVGL's SPDX 3.0 SBOM against the official SPDX JSON Schema.
This is the validation method recommended by SPDX itself: check the document
against the published JSON Schema for its spec version. It uses check-jsonschema
(https://github.com/python-jsonschema/check-jsonschema) as the engine:
python3 -m pip install check-jsonschema
Usage:
python3 scripts/validate_sbom.py [SBOM ...]
[--schema URL_OR_PATH] [--spec-version 3.0.1]
With no SBOM argument sbom/lvgl.spdx.json is validated. The schema
defaults to the official SPDX schema for --spec-version; pass a local path with
--schema to validate offline.
"""
import argparse
import glob
import importlib.util
import os
import subprocess
import sys
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SBOM_DIR = os.path.join(REPO_ROOT, "sbom")
SCHEMA_URL = "https://spdx.org/schema/%s/spdx-json-schema.json"
def main():
parser = argparse.ArgumentParser(
description="Validate LVGL's SPDX SBOM against the official SPDX JSON Schema.")
parser.add_argument("sboms", nargs="*",
help="SBOM file(s) to validate (default: sbom/lvgl.spdx.json).")
parser.add_argument("--spec-version", default="3.0.1",
help="SPDX spec version whose schema to use (default: 3.0.1).")
parser.add_argument("--schema", default=None,
help="Override the schema URL or local file path.")
args = parser.parse_args()
sboms = args.sboms or sorted(glob.glob(os.path.join(SBOM_DIR, "lvgl.spdx.json")))
if not sboms:
print("No SBOM files found in %s (run scripts/generate_sbom.py first)." % SBOM_DIR)
return 1
missing = [p for p in sboms if not os.path.isfile(p)]
if missing:
print("SBOM file(s) not found: %s" % ", ".join(missing))
return 1
# Launching `python -m check_jsonschema` when the module is absent exits
# with the interpreter's own error code (not FileNotFoundError), so detect
# the module up front to surface the documented install instructions.
if importlib.util.find_spec("check_jsonschema") is None:
print("check-jsonschema is not installed. Install it with:\n"
" python3 -m pip install check-jsonschema")
return 2
schema = args.schema or (SCHEMA_URL % args.spec_version)
cmd = [sys.executable, "-m", "check_jsonschema", "--schemafile", schema] + sboms
print("Validating against SPDX %s schema:\n %s\n" % (args.spec_version, schema))
return subprocess.run(cmd).returncode
if __name__ == "__main__":
sys.exit(main())