Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 123 additions & 8 deletions tools/export_pkg_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,15 @@
# 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

import argparse
import hashlib
import json
import shutil
import struct
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List
Expand All @@ -41,6 +35,13 @@
"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:
Expand All @@ -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 "
Expand All @@ -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
Expand All @@ -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"],
Expand Down Expand Up @@ -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(
Expand All @@ -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()
Expand All @@ -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)
Expand All @@ -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(
Expand Down
Loading