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(

Reply via email to