mirror of
https://github.com/lvgl/lvgl.git
synced 2026-08-18 02:38:01 +08:00
docs(cra): add SBOM for CRA complience (#10352)
This commit is contained in:
@@ -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."
|
||||
|
||||
@@ -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
@@ -1,67 +1,88 @@
|
||||
LVGL uses the following third-party libraries.
|
||||
For the licenses, see the corresponding `LICENSE.txt` file in each library’s 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
@@ -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).
|
||||
@@ -5,6 +5,7 @@
|
||||
"requirements",
|
||||
"license",
|
||||
"policies",
|
||||
"security",
|
||||
"faq"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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"}
|
||||
}
|
||||
]
|
||||
}
|
||||
Executable
+94
@@ -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())
|
||||
Executable
+392
@@ -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())
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user