Skip to content

Commit de1a59a

Browse files
committed
tools: Export package metadata and RGB565 icons.
Keep the existing package specification syntax backward compatible, including source paths containing colons. Add separate name-keyed options for optional descriptions, categories, and icon source images. Convert icons to a fixed 48 by 48 little-endian RGB565 payload with an LVGL-compatible image header, and place them at package-level repository paths for nxstore caching. Assisted-by: OpenAI Codex:gpt-5.6-sol Signed-off-by: aviralgarg05 <gargaviral99@gmail.com>
1 parent 5e09f0b commit de1a59a

1 file changed

Lines changed: 122 additions & 0 deletions

File tree

tools/export_pkg_repo.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import hashlib
3333
import json
3434
import shutil
35+
import struct
3536
from dataclasses import dataclass
3637
from pathlib import Path
3738
from typing import Dict, List
@@ -41,6 +42,21 @@
4142
"package must look like " "<name>:<version>:<elf|shared-lib>:<source>"
4243
)
4344

45+
# Fixed square icon size nxstore renders app-list icons at (see
46+
# nxstore_load_icon() in system/nxstore/nxstore_main.c) - small enough to
47+
# stay a quick download over the board's WiFi, large enough to read as
48+
# actual artwork rather than a favicon.
49+
ICON_SIZE = 48
50+
51+
# Matches lv_image_header_t's own bit layout (LVGL v9, see
52+
# graphics/lvgl/lvgl/src/draw/lv_image_dsc.h): magic/cf/flags packed into
53+
# the first 4 bytes, then w/h, then stride/reserved - all little-endian,
54+
# 12 bytes total, immediately followed by raw pixel data. This is
55+
# deliberately the simplest format LVGL can render directly with no
56+
# decoder, since the board has no PNG/JPEG decode capability wired up.
57+
LV_IMAGE_HEADER_MAGIC = 0x19
58+
LV_COLOR_FORMAT_RGB565 = 0x12
59+
4460

4561
@dataclass
4662
class PackageSpec:
@@ -88,6 +104,24 @@ def parse_package_spec(value: str) -> PackageSpec:
88104
)
89105

90106

107+
def parse_name_value(value: str) -> tuple:
108+
parts = value.split("=", 1)
109+
if len(parts) != 2 or not parts[0] or not parts[1]:
110+
raise argparse.ArgumentTypeError("value must look like <name>=<text>")
111+
112+
return parts[0], parts[1]
113+
114+
115+
def parse_icon_spec(value: str) -> tuple:
116+
name, source = parse_name_value(value)
117+
source_path = Path(source).expanduser().resolve()
118+
if not source_path.is_file():
119+
msg = f"icon source does not exist: {source_path}"
120+
raise argparse.ArgumentTypeError(msg)
121+
122+
return name, source_path
123+
124+
91125
def artifact_relpath(arch: str, chip: str, compat: str,
92126
spec: PackageSpec) -> Path: # fmt: skip
93127
filename = spec.source.name
@@ -96,6 +130,47 @@ def artifact_relpath(arch: str, chip: str, compat: str,
96130
return path
97131

98132

133+
def icon_relpath(arch: str, chip: str, compat: str, name: str) -> Path:
134+
# Keep one repository object per package. nxstore keys its local cache
135+
# by package name and version, so a new catalog version fetches this
136+
# object again without requiring versioned repository duplication.
137+
return Path("icons") / arch / chip / compat / f"{name}.bin"
138+
139+
140+
def encode_icon_rgb565(source: Path, size: int = ICON_SIZE) -> bytes:
141+
"""Convert an arbitrary source image into nxstore's raw icon format:
142+
a 12-byte lv_image_header_t-compatible header (see the module
143+
docstring comment above) followed by size*size RGB565 pixel data.
144+
"""
145+
146+
from PIL import Image
147+
148+
with Image.open(source) as img:
149+
img = img.convert("RGB").resize((size, size), Image.Resampling.LANCZOS)
150+
pixels = list(img.getdata())
151+
152+
stride = size * 2
153+
header = struct.pack(
154+
"<BBHHHHH",
155+
LV_IMAGE_HEADER_MAGIC,
156+
LV_COLOR_FORMAT_RGB565,
157+
0, # flags
158+
size, # w
159+
size, # h
160+
stride,
161+
0, # reserved_2
162+
)
163+
164+
body = bytearray(stride * size)
165+
offset = 0
166+
for r, g, b in pixels:
167+
rgb565 = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
168+
struct.pack_into("<H", body, offset, rgb565)
169+
offset += 2
170+
171+
return header + bytes(body)
172+
173+
99174
def package_identity(package: Dict[str, str]) -> tuple:
100175
return (
101176
package["name"],
@@ -171,6 +246,30 @@ def main() -> int:
171246
type=parse_package_spec,
172247
help=PACKAGE_SPEC_HELP,
173248
)
249+
parser.add_argument(
250+
"--package-description",
251+
action="append",
252+
default=[],
253+
type=parse_name_value,
254+
metavar="NAME=TEXT",
255+
help="Optional package description, keyed by package name",
256+
)
257+
parser.add_argument(
258+
"--package-category",
259+
action="append",
260+
default=[],
261+
type=parse_name_value,
262+
metavar="NAME=TEXT",
263+
help="Optional package category, keyed by package name",
264+
)
265+
parser.add_argument(
266+
"--package-icon",
267+
action="append",
268+
default=[],
269+
type=parse_icon_spec,
270+
metavar="NAME=PATH",
271+
help="Optional source image for a package icon",
272+
)
174273
args = parser.parse_args()
175274

176275
repo_dir = args.repo_dir.expanduser().resolve()
@@ -181,6 +280,9 @@ def main() -> int:
181280
for package in packages:
182281
packages_by_id[package_identity(package)] = package
183282
prefix = args.artifact_prefix.rstrip("/")
283+
descriptions = dict(args.package_description)
284+
categories = dict(args.package_category)
285+
icons = dict(args.package_icon)
184286

185287
for spec in args.package:
186288
relpath = artifact_relpath(args.arch, args.chip, args.compat, spec)
@@ -201,6 +303,26 @@ def main() -> int:
201303
"sha256": sha256_file(destination),
202304
"type": spec.payload_type,
203305
}
306+
307+
if spec.name in descriptions:
308+
package["description"] = descriptions[spec.name]
309+
310+
if spec.name in categories:
311+
package["category"] = categories[spec.name]
312+
313+
if spec.name in icons:
314+
icon_bytes = encode_icon_rgb565(icons[spec.name])
315+
icon_dest = repo_dir / icon_relpath(
316+
args.arch, args.chip, args.compat, spec.name
317+
)
318+
icon_dest.parent.mkdir(parents=True, exist_ok=True)
319+
icon_dest.write_bytes(icon_bytes)
320+
321+
icon_rel = icon_relpath(
322+
args.arch, args.chip, args.compat, spec.name
323+
).as_posix()
324+
package["icon"] = f"{prefix}/{icon_rel}" if prefix else icon_rel
325+
204326
packages_by_id[package_identity(package)] = package
205327

206328
packages = sorted(

0 commit comments

Comments
 (0)