This is an automated email from the ASF dual-hosted git repository.
acassis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nuttx-apps.git
The following commit(s) were added to refs/heads/master by this push:
new c0143f0f4 tools: export package metadata and RGB565 icons
c0143f0f4 is described below
commit c0143f0f460addbf4dfc8310253317266b654930
Author: aviralgarg05 <[email protected]>
AuthorDate: Sat Aug 15 16:03:16 2026 +0530
tools: export package metadata and RGB565 icons
Write optional package metadata and convert icons to bounded RGB565 assets.
Reject paths outside the package directory.
Signed-off-by: aviralgarg05 <[email protected]>
---
tools/export_pkg_repo.py | 131 ++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 123 insertions(+), 8 deletions(-)
diff --git a/tools/export_pkg_repo.py b/tools/export_pkg_repo.py
index 36d611434..2f2c674fe 100644
--- a/tools/export_pkg_repo.py
+++ b/tools/export_pkg_repo.py
@@ -17,14 +17,7 @@
# License for the specific language governing permissions and limitations
# under the License.
-"""Export built binaries into a package repository layout.
-
-The script copies built application or library artifacts into a
-server-friendly repository directory, computes SHA-256 digests, and emits an
-`index.json` compatible with the current `nxpkg` metadata parser.
-
-The repository can then be served by FTP, HTTP, or any static file transport.
-"""
+"""Build an nxpkg repository from compiled artifacts."""
from __future__ import annotations
@@ -32,6 +25,7 @@ import argparse
import hashlib
import json
import shutil
+import struct
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List
@@ -41,6 +35,13 @@ PACKAGE_SPEC_HELP = (
"package must look like " "<name>:<version>:<elf|shared-lib>:<source>"
)
+# Keep icons small for target downloads.
+ICON_SIZE = 48
+
+# LVGL v9 lv_image_header_t fields, followed by raw RGB565 pixels.
+LV_IMAGE_HEADER_MAGIC = 0x19
+LV_COLOR_FORMAT_RGB565 = 0x12
+
@dataclass
class PackageSpec:
@@ -63,12 +64,21 @@ def sha256_file(path: Path) -> str:
return digest.hexdigest()
+def parse_component(value: str) -> str:
+ if not value or value in {".", ".."} or "/" in value or "\\" in value:
+ raise argparse.ArgumentTypeError("value must be one path component")
+
+ return value
+
+
def parse_package_spec(value: str) -> PackageSpec:
parts = value.split(":", 3)
if len(parts) != 4:
raise argparse.ArgumentTypeError(PACKAGE_SPEC_HELP)
name, version, payload_type, source = parts
+ name = parse_component(name)
+ version = parse_component(version)
if payload_type not in VALID_TYPES:
raise argparse.ArgumentTypeError(
f"unsupported package type '{payload_type}', expected one of "
@@ -88,6 +98,25 @@ def parse_package_spec(value: str) -> PackageSpec:
)
+def parse_name_value(value: str) -> tuple:
+ parts = value.split("=", 1)
+ if len(parts) != 2 or not parts[0] or not parts[1]:
+ raise argparse.ArgumentTypeError("value must look like <name>=<text>")
+
+ return parts[0], parts[1]
+
+
+def parse_icon_spec(value: str) -> tuple:
+ name, source = parse_name_value(value)
+ name = parse_component(name)
+ source_path = Path(source).expanduser().resolve()
+ if not source_path.is_file():
+ msg = f"icon source does not exist: {source_path}"
+ raise argparse.ArgumentTypeError(msg)
+
+ return name, source_path
+
+
def artifact_relpath(arch: str, chip: str, compat: str,
spec: PackageSpec) -> Path: # fmt: skip
filename = spec.source.name
@@ -96,6 +125,42 @@ def artifact_relpath(arch: str, chip: str, compat: str,
return path
+def icon_relpath(arch: str, chip: str, compat: str, name: str) -> Path:
+ # The client refreshes this object when the catalog version changes.
+ return Path("icons") / arch / chip / compat / f"{name}.bin"
+
+
+def encode_icon_rgb565(source: Path, size: int = ICON_SIZE) -> bytes:
+ """Encode an image as an LVGL header and RGB565 pixels."""
+
+ from PIL import Image
+
+ with Image.open(source) as img:
+ img = img.convert("RGB").resize((size, size), Image.Resampling.LANCZOS)
+ pixels = list(img.getdata())
+
+ stride = size * 2
+ header = struct.pack(
+ "<BBHHHHH",
+ LV_IMAGE_HEADER_MAGIC,
+ LV_COLOR_FORMAT_RGB565,
+ 0, # flags
+ size, # w
+ size, # h
+ stride,
+ 0, # reserved_2
+ )
+
+ body = bytearray(stride * size)
+ offset = 0
+ for r, g, b in pixels:
+ rgb565 = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
+ struct.pack_into("<H", body, offset, rgb565)
+ offset += 2
+
+ return header + bytes(body)
+
+
def package_identity(package: Dict[str, str]) -> tuple:
return (
package["name"],
@@ -147,16 +212,19 @@ def main() -> int:
parser.add_argument(
"--arch",
required=True,
+ type=parse_component,
help="Target architecture string, for example xtensa",
)
parser.add_argument(
"--chip",
required=True,
+ type=parse_component,
help="Target chip/family string, for example esp32s3",
)
parser.add_argument(
"--compat",
required=True,
+ type=parse_component,
help="Target board/runtime identity, for example esp32s3-xiao",
)
parser.add_argument(
@@ -171,6 +239,30 @@ def main() -> int:
type=parse_package_spec,
help=PACKAGE_SPEC_HELP,
)
+ parser.add_argument(
+ "--package-description",
+ action="append",
+ default=[],
+ type=parse_name_value,
+ metavar="NAME=TEXT",
+ help="Optional package description, keyed by package name",
+ )
+ parser.add_argument(
+ "--package-category",
+ action="append",
+ default=[],
+ type=parse_name_value,
+ metavar="NAME=TEXT",
+ help="Optional package category, keyed by package name",
+ )
+ parser.add_argument(
+ "--package-icon",
+ action="append",
+ default=[],
+ type=parse_icon_spec,
+ metavar="NAME=PATH",
+ help="Optional source image for a package icon",
+ )
args = parser.parse_args()
repo_dir = args.repo_dir.expanduser().resolve()
@@ -181,6 +273,9 @@ def main() -> int:
for package in packages:
packages_by_id[package_identity(package)] = package
prefix = args.artifact_prefix.rstrip("/")
+ descriptions = dict(args.package_description)
+ categories = dict(args.package_category)
+ icons = dict(args.package_icon)
for spec in args.package:
relpath = artifact_relpath(args.arch, args.chip, args.compat, spec)
@@ -201,6 +296,26 @@ def main() -> int:
"sha256": sha256_file(destination),
"type": spec.payload_type,
}
+
+ if spec.name in descriptions:
+ package["description"] = descriptions[spec.name]
+
+ if spec.name in categories:
+ package["category"] = categories[spec.name]
+
+ if spec.name in icons:
+ icon_bytes = encode_icon_rgb565(icons[spec.name])
+ icon_dest = repo_dir / icon_relpath(
+ args.arch, args.chip, args.compat, spec.name
+ )
+ icon_dest.parent.mkdir(parents=True, exist_ok=True)
+ icon_dest.write_bytes(icon_bytes)
+
+ icon_rel = icon_relpath(
+ args.arch, args.chip, args.compat, spec.name
+ ).as_posix()
+ package["icon"] = f"{prefix}/{icon_rel}" if prefix else icon_rel
+
packages_by_id[package_identity(package)] = package
packages = sorted(