commit 5fe41e79e942d49347b69d548b2ee0b5d07fee7d Author: sdctech <1+sdctech@noreply.localhost> Date: Mon Aug 17 18:42:29 2026 +0000 Initial RK1 media-center image project diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a4e27f --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +/dist/ +/downloads/ +/sources/ +/work/ +/armbian/.work/ +/media/out/ + +# Public keys are source inputs; private credentials never belong in Git. +/keys/* +!/keys/*.pub +*.pem +*.key +.env +.env.* +!.env.example diff --git a/ACCEPTANCE.md b/ACCEPTANCE.md new file mode 100644 index 0000000..bc00b6b --- /dev/null +++ b/ACCEPTANCE.md @@ -0,0 +1,105 @@ +# Physical acceptance test + +The image has passed structural checks. Run this checklist on the RK1 attached +to the actual carrier and TV before treating it as the permanent media center. + +## 1. Provisioning and services + +SSH over Ethernet and wait for provisioning to finish: + +```bash +ssh rkadmin@rk1-media.local +sudo systemctl is-active rk1-media-identity.service +sudo systemctl is-active rk1-media-provision.service +sudo systemctl is-active kodi-rk.service +sudo journalctl -u rk1-media-provision.service -u kodi-rk.service -b --no-pager +``` + +All three should be active/successful and Kodi should be visible. The first +boot may remain on a console while packages install. + +## 2. One-command hardware qualification + +```bash +sudo rk1-media-selftest +sudo rk1-media-selftest --strict --json | tee /tmp/rk1-selftest.json +``` + +The normal run performs short Vulkan, hardware encode, and NPU workloads when +applicable. Strict mode treats a missing/disconnected advertised device as a +failure. Confirm Panthor, an HDMI connector with EDID/modes, RKMPP decoder and +encoder bindings, RGA2/RGA3, RKNPU, HDMI ALSA, Ethernet, eMMC, and NVMe. + +Run the NPU directly across all three cores: + +```bash +rknn-inference-test --core all --iterations 10 +``` + +## 3. 4K media acceptance + +Use media you legally possess, with filenames containing `h264`, `hevc`, +`vp9`, and `av1` so the self-test can select them: + +```bash +sudo rk1-media-selftest --media-dir /path/to/test-clips +ffmpeg-rk -hide_banner -decoders | grep rkmpp +ffmpeg-rk -hide_banner -encoders | grep rkmpp +ffmpeg-rk -hide_banner -filters | grep rkrga +``` + +In Kodi, test at least 20 minutes each of 4K60 SDR H.264/HEVC, VP9, and AV1, +plus seeking, pause/resume, subtitles, and HDMI PCM audio. Watch CPU usage and +temperature; hardware-decoded playback should not pin all CPU cores. Also test +CEC with `cec-client -l` and the intended remote/IR receiver. + +HDR, lossless audio passthrough, HDCP/streaming-service DRM, and unusual +10/12-bit chroma formats are separate acceptance items; this build does not +claim them merely because 4K60 SDR succeeds. + +## 4. Blank-HDMI decision tree + +From SSH: + +```bash +for connector in /sys/class/drm/card*-HDMI-A-*; do + printf '%s: ' "$connector" + cat "$connector/status" + cat "$connector/modes" +done +sudo journalctl -b -k | grep -Ei 'drm|hdmi|edid|vop|hdptx|panthor' +sudo journalctl -b -u kodi-rk.service --no-pager +``` + +- No HDMI connector: DTB/kernel probe issue; collect diagnostics. +- Connector says `disconnected`: focus on carrier HPD/DDC, cable, TV input, and + the early-RK1 hardware note below—not Kodi. +- `connected` but no modes: EDID/DDC or sink/cable issue. +- Modes exist but Kodi loops: inspect DRM permissions/master and Kodi logs at + `/var/lib/kodi/.kodi/temp/kodi.log`. +- Kodi runs but the screen is black: stop Kodi and test direct KMS/Vulkan, then + collect a bundle. + +```bash +sudo systemctl stop kodi-rk.service +sudo kmscube +sudo systemctl start kodi-rk.service +sudo rk1-media-diagnostics +``` + +The diagnostic command writes locally and does not upload anything. + +## Carrier/module caveat + +The supplied Amazon ASIN is marketed as a KLAYERS Jetson **Orin** Nano/NX HDMI +base board, rather than NVIDIA’s older Jetson Nano carrier. Its advertised +layout matches the relevant Jetson-compatible RK1 HDMI lane/control pins, but a +public KLAYERS schematic was not available, so physical validation is still +required. + +Turing documents that modules from its initial pool of 100 RK1s can fail HDMI +with some Jetson-compatible carriers and may need two solder bridges. Do not +guess the bridge locations: verify the module batch and obtain Turing’s exact +hardware guidance before modifying it: + +https://docs.turingpi.com/docs/turing-rk1-flashing-os diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..28fb63e --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,65 @@ +# Design and source lock + +## Production artifact inputs + +- Armbian Community 26.11.0-trunk.1 Turing RK1 Trixie minimal image, vendor + kernel 6.1.115: SHA-256 + `cbbd16d90786331fbfbbb695614a53f5e87ea441ea37b425d986334318190cf6`. +- Repacked Kodi/RKMPP appliance package: SHA-256 + `dd0ca68696aa35b1998f9c9e131fdf8f0d1ef4ba6f58a36d0d99f0625d47f430`. +- RKNN Toolkit2 v2.3.2 commit + `42aa1d426c0a9e0869b6374edba009f7208a1926`, with per-asset checksums. +- 140 Debian Trixie dependency packages locked by filename, size, SHA-256, and + signed-index URL in `packages/debian-packages.lock.tsv`. + +The upstream Kodi release recipe followed moving branches, so its immutable +release asset SHA-256 is the authoritative input for the fast repack path. A +fully source-pinned native build recipe is retained with these commits: + +- MPP `a9380ef333102ac318628f83b5f7a460d377749e` +- RGA `1d330cc28551943bed3380261a5a9c6fbd58ff53` +- FFmpeg-Rockchip `d90e3a1c18d7929383cf88c1b3da2e2d1c966cbf` +- dav1d `b546257f770768b2c88258c533da38b91a06f737` +- libdisplay-info `f1b75310181e364f97a16464efcd92bc82e57ccc` +- Kodi `9c56bf593441a6c246fdc6931177e71f349ee2bc` + +The source-level Armbian base recipe separately pins: + +- Armbian framework `ea18947bed789c829a260df129e815c353c14908` +- Rockchip kernel 6.1.115 `5280f9b4336199c4025c8eed894d2b4e2268dcc6` +- U-Boot `ece349ade2973e220f524ce59e59711cc919263f` +- RKBin `452f49a987097d7ca675811e51a873b42ebd101f` +- Armbian firmware `d9846710f54da5e4383e2d67311819659ac2cf5c` + +## Trust and update boundaries + +The vendor kernel, Rockchip binary boot components, RKNN runtime, and upstream +Kodi media integration are trusted community/vendor inputs, not Debian or +LibreELEC-supported RK1 components. Each is pinned and isolated, but pins do not +turn proprietary firmware/runtime blobs into auditable source. + +Debian security updates run automatically. Kernel, DTB, U-Boot/BSP, Mesa/media, +and RKNN changes are deliberately excluded from unattended upgrades because +they must move together and pass the physical acceptance suite. Rebuild and +requalify the image to update those layers. + +The open GPU and proprietary GPU stacks are mutually exclusive in practice: +Panthor/Panfrost/PanVK is selected for KMS, OpenGL, Vulkan, and Kodi stability; +Mali kbase/libMali/OpenCL is not installed. NPU compute is provided separately +by RKNN. + +## Security posture + +- No default or image-time password remains; root’s shadow field is exactly + `!` before the raw image is emitted. +- SSH accepts public keys only, refuses root, and is ordered after unique host + key generation. The identity unit creates SSH's `/run/sshd` runtime directory + before validating the daemon configuration; `ssh.service` recreates it for + its own lifetime. Armbian’s later first-run key regeneration is disabled. +- The build rejects private-key input and authorized-key options. +- Kodi runs under a locked system user with device groups and systemd sandboxing. +- Package and injected-directory modes are normalized and asserted so a + collaborative host umask cannot make `/`, `/etc`, `/usr`, or `/opt` + group-writable. +- First-boot package installation is fully offline; APT simulation proves its + dependency closure before image creation. diff --git a/FLASHING.md b/FLASHING.md new file mode 100644 index 0000000..c0eef7a --- /dev/null +++ b/FLASHING.md @@ -0,0 +1,83 @@ +# Flashing the image to RK1 eMMC + +Flashing destroys the selected target. Boot the RK1 from another device first +(for example NVMe), or attach the eMMC through an appropriate loader/USB mode. +Never overwrite the device that currently contains `/`. + +Identify every disk by path, size, model, and serial: + +```bash +findmnt / +lsblk -o NAME,PATH,SIZE,MODEL,SERIAL,TRAN,TYPE,MOUNTPOINTS +``` + +The eMMC will usually be a whole `/dev/mmcblkN` device, but the number is not a +promise. The helper deliberately refuses partitions such as `/dev/mmcblk0p1`, +non-MMC disks, mounted descendants, and active swap. Repeat the resolved target +path in `--confirm`: + +```bash +cd /home/ubuntu/rk1-media-image +sudo ./scripts/flash-emmc.sh \ + --image dist/rk1-media-20260817-r4-trixie-vendor-6.1.115-turing-rk1-emmc.img.xz \ + --target /dev/mmcblkN \ + --confirm /dev/mmcblkN +``` + +The helper performs four checks around the destructive write: + +1. validates the XZ stream and adjacent SHA-256 file; +2. checks target type, capacity, mounts, and swap; +3. streams the 4 GiB raw image with a flushed direct write; +4. decompresses it again while reading the target back byte-for-byte. + +Only remove power after it reports that read-back verification passed. Then +shut down, remove or reprioritize the temporary boot disk, attach Ethernet and +HDMI, turn on the TV/input, and boot the eMMC. + +If the carrier exposes RK1 eMMC through a different flashing workflow, the +`.img.xz` is a normal whole-disk image. A trusted imager can use it directly; +retain the same checksum and target-identification discipline. + +## Keep boot on eMMC and move root to NVMe + +The RK1 still needs its boot payload and `/boot` on eMMC. Once the appliance has +finished first-boot provisioning, identify the NVMe by its exact model and +serial, then run the helper included in revision r4: + +```bash +findmnt -no SOURCE,FSTYPE / +lsblk -e7 -o NAME,PATH,SIZE,TYPE,FSTYPE,MOUNTPOINTS,MODEL,SERIAL +sudo test -e /var/lib/rk1-media/provisioned && echo PROVISIONED + +sudo rk1-media-migrate-root-to-nvme \ + --target /dev/nvme0n1 \ + --serial EXACT_SERIAL_FROM_LSBLK +``` + +This destroys every existing partition on only the confirmed target NVMe. It +does not repartition the eMMC or rewrite U-Boot. The helper stops Kodi and +package-update jobs, performs the Armbian `sd`-mode root copy, checks the new +filesystem and boot configuration independently, and prints +`MIGRATION_VALIDATED` only after the rollback-sensitive writes have synced. +Reboot immediately after that result: + +```bash +sudo systemctl reboot +findmnt -no SOURCE,FSTYPE / +findmnt /boot +findmnt /media/boot-media +``` + +To return to the intact eMMC root while the NVMe-rooted system is still +reachable: + +```bash +sudo cp -a /boot/armbianEnv.txt.pre-nvme /boot/armbianEnv.txt +sudo cmp /boot/armbianEnv.txt.pre-nvme /boot/armbianEnv.txt +sudo sync +sudo systemctl reboot +``` + +Never use `armbian-install --boot split-emmc` for this rollback-preserving +migration; that mode repartitions both devices. diff --git a/README.md b/README.md new file mode 100644 index 0000000..45dce6f --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ +# Turing RK1 media-center image + +This project now produces a complete, flashable home-theater image for a +Turing RK1 on a Jetson Orin Nano/NX-compatible HDMI carrier. It uses Debian 13 +(Trixie) userspace on Armbian Community with the Rockchip vendor 6.1.115 kernel, +then starts Kodi directly on DRM/KMS through GBM—there is no desktop or display +manager in the way. + +The intentionally non-obvious choice is the kernel. Armbian also offers newer +mainline kernels, but the vendor 6.1 branch is currently the coherent path for +RK3588 HDMI, RKMPP VPU, RGA, and RKNPU support. “Newest kernel number” and “most +complete RK3588 appliance” are not the same target. + +## Ready artifact + +- `dist/rk1-media-20260817-r4-trixie-vendor-6.1.115-turing-rk1-emmc.img.xz` +- SHA-256: `bb4fc337f1293fb2b2423b34e8fff370ed69e9ec8709c7da87fdfeea2a516f04` +- Compressed size: 591,529,876 bytes; raw image: 4 GiB +- Initial target: whole eMMC (the included guarded helper can then move `/` + to NVMe while retaining RK1 U-Boot and `/boot` on eMMC) +- Login: `rkadmin` using the sole public key whose fingerprint is recorded in + the adjacent manifest +- Hostname after first boot: `rk1-media.local` + +There is no password login and no root login. The image contains no SSH private +key and no image-time SSH host key. Unique host keys are generated before SSH +can start. `rkadmin` has passwordless sudo because its only authentication path +is a supplied public key. + +Read [FLASHING.md](FLASHING.md) before writing eMMC. The included flasher +requires the target path twice, rejects mounted targets, verifies the compressed +image, and reads the written bytes back. + +## Hardware stack + +- GPU: Panthor kernel DRM plus Mesa Panfrost/PanVK (OpenGL ES/OpenGL/Vulkan). +- Video: matched RKMPP decode/encode and RGA zero-copy/scaling stack isolated + under `/opt/rkmedia`; system FFmpeg libraries are not replaced. +- Kodi: direct GBM/GLES appliance service on tty1, running as a locked `kodi` + user rather than root. +- Decode: H.264, HEVC, VP9, AV1, and MJPEG RKMPP paths. +- Encode: H.264, HEVC, and MJPEG RKMPP paths. +- NPU: pinned RKNN Runtime 2.3.2, RK3588 model, per-core inference test, and + local diagnostics. +- HDMI support: exact `rk3588-turing-rk1.dtb`, HDMI PHY/audio nodes, Panthor + overlay, ALSA, CEC utilities, and IR keymap support. + +The open Panthor/Mesa GPU route does not provide Mali OpenCL. Proprietary +libMali/OpenCL requires the mutually incompatible Mali kbase stack; it is not +mixed into this media image. The RK3588 NPU remains available through RKNN. + +## Build it again + +On an ARM64 build host: + +```bash +cd /home/ubuntu/rk1-media-image +./build.sh --ssh-public-key /absolute/path/to/authorized_keys +``` + +The key file can hold multiple plain OpenSSH public-key lines. Private keys and +authorized-key options are rejected. The build needs no root access, mounts, or +loop devices. It verifies/downloads locked inputs, verifies 140 locked Debian +dependency packages, reproducibly repacks the Kodi bundle, installs the RKNN +runtime into the filesystem, expands the GPT/ext4 image, and emits checksums and +a manifest under `dist/`. + +Validation: + +```bash +./tests/run-static.sh +./tests/validate-image.sh \ + dist/rk1-media-20260817-r4-trixie-vendor-6.1.115-turing-rk1-emmc.img +``` + +The produced image uses a checksum-locked Armbian release asset. `armbian/` +also contains a source-level base-image recipe pinned to the Armbian framework, +kernel, U-Boot, RKBin, and firmware commits. Archive package versions are +captured separately; true byte-for-byte source rebuilds additionally require a +package-repository snapshot. + +## First boot + +Connect Ethernet and HDMI before powering on. Armbian expands the root +filesystem, creates the unique machine/SSH identity, and makes SSH available. +An offline one-shot service then installs the 141-package graphics/media bundle, +reloads device permissions, enables Avahi, and starts Kodi. This can take a few +minutes; do not remove power while `rk1-media-provision.service` is active. + +```bash +ssh rkadmin@rk1-media.local +systemctl status rk1-media-provision.service kodi-rk.service +journalctl -u rk1-media-provision.service -u kodi-rk.service -b +``` + +Revision r4 includes the SSH runtime-directory correction introduced in r2, +fixes first-boot offline APT acquisition and Kodi service ordering, and contains +only the dedicated `rk1-media` public key. Its fingerprint is +`SHA256:12b/wcqo8/yLfZ2OnLYga9baOt6tIlOSVZWrnjXtuN4`. The superseded +unrevisioned image could leave port 22 closed, while r3 could fail during its +offline package transaction and then deadlock while starting Kodi. Neither +should be used for a new flash; the repaired live r3 system does not need to be +reflashed. + +The provisioner retries on the next boot if it does not reach its success +marker. Platform kernel/DTB/U-Boot packages are held; Debian security updates +are automatic, while platform/media upgrades require an intentionally tested +image rebuild. + +Continue with [ACCEPTANCE.md](ACCEPTANCE.md). Static validation cannot prove +the carrier’s physical HPD/DDC/TMDS path, the TV’s EDID, DRM-master acquisition, +or sustained 4K acceleration. + +## Move the root filesystem to NVMe + +Flash and boot the image from eMMC first. After first-boot provisioning has +completed, the image includes a guarded migration helper that keeps U-Boot and +`/boot` on eMMC, creates a fresh ext4 filesystem on the selected NVMe, copies +the live root, and validates the UUIDs, filesystem, `fstab`, and boot environment +before allowing a reboot: + +```bash +findmnt -no SOURCE,FSTYPE / +lsblk -e7 -o NAME,PATH,SIZE,TYPE,FSTYPE,MOUNTPOINTS,MODEL,SERIAL +sudo rk1-media-migrate-root-to-nvme \ + --target /dev/nvme0n1 \ + --serial EXACT_SERIAL_FROM_LSBLK +sudo systemctl reboot +``` + +The selected NVMe is erased. Do not use Armbian's `split-emmc` mode for this +workflow: the eMMC is the boot medium and rollback copy. After reboot, +`findmnt /` should report the NVMe partition while `/boot` and +`/media/boot-media` resolve to eMMC. See [FLASHING.md](FLASHING.md) for the +rollback command. + +## Project layout + +- `image/`: rootless raw-image composition and first-boot services. +- `media/`: deterministic release repack and fully pinned native source recipe. +- `runtime/`: RKNN runtime installer, NPU test, hardware self-test, diagnostics. +- `packages/`: exact Debian dependency integrity lock and fetcher. +- `armbian/`: source-pinned Armbian base-image recipe. +- `tests/`: package, dependency, runtime, and loopless image validation. +- `scripts/`: locked input fetcher and guarded eMMC flasher. + +See [DESIGN.md](DESIGN.md) for pins, trust boundaries, and tradeoffs. diff --git a/armbian/.gitignore b/armbian/.gitignore new file mode 100644 index 0000000..ca978d0 --- /dev/null +++ b/armbian/.gitignore @@ -0,0 +1,2 @@ +.work/ + diff --git a/armbian/README.md b/armbian/README.md new file mode 100644 index 0000000..09e847a --- /dev/null +++ b/armbian/README.md @@ -0,0 +1,57 @@ +# Armbian base image for Turing RK1 + +This directory builds the minimal, remotely manageable base for the RK1 media +appliance. It targets Debian 13 (Trixie), Armbian's Rockchip vendor kernel, and +the `rk3588-turing-rk1.dtb`. Kodi and the pinned RKMPP/RGA media stack are added +by the higher-level project layers. + +## Build + +Use a dedicated administrator public key. Password and root login are disabled +in the resulting image. + +```bash +./build-image.sh --ssh-public-key /absolute/path/to/id_ed25519.pub +``` + +Useful non-building checks: + +```bash +./tests/lint.sh +./build-image.sh --ssh-public-key /absolute/path/to/id_ed25519.pub --prepare-only +./build-image.sh --ssh-public-key /absolute/path/to/id_ed25519.pub --config-dump +``` + +The framework checkout, source caches, build logs, and images live under +`.work/armbian-build/`; completed images are in its `output/images/` directory. +The build command creates an image and checksum but never flashes a device. + +## What is locked + +`userpatches/source-lock.env` pins the Armbian framework, the 6.1.115 Rockchip +kernel source, U-Boot, Rockchip's required binary boot components, and Armbian +firmware by commit hash. The custom `rkbin-tools` extension and framework +`git_sources.json` override prevent the two stock moving `master` inputs from +silently changing. + +Debian and Armbian archive packages are resolved when the image is built, so +byte-for-byte reproduction also requires retaining the generated package +manifest and a snapshot of the package repositories. Every built image records +the resolved package versions in `/usr/share/rk1-media/package-manifest.tsv`. + +## First boot and updates + +- `rkadmin` is created with the supplied SSH public key and passwordless sudo. +- Root and user password hashes are replaced with an unusable value. +- SSH password, keyboard-interactive, and root authentication are disabled. +- Image-time SSH host keys are erased and regenerated before SSH starts. +- Ethernet uses Armbian's systemd-networkd DHCP configuration. +- Avahi advertises the fixed hostname `rk1-media.local`. +- `overlays=panthor-gpu` selects Mesa's Panthor DRM path instead of libMali. +- Armbian's independent filesystem-resize service remains enabled. +- Debian security updates are automatic; kernel, boot, BSP, and media packages + remain held until a rebuilt image passes hardware acceptance tests. + +The raw Armbian image is suitable for eMMC, NVMe, or removable-media flashing. +Always identify the destination by model, size, and serial immediately before a +write; the build wrapper deliberately contains no flashing operation. diff --git a/armbian/build-image.sh b/armbian/build-image.sh new file mode 100755 index 0000000..03734f3 --- /dev/null +++ b/armbian/build-image.sh @@ -0,0 +1,164 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +IFS=$'\n\t' + +readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +readonly PROJECT_USERPATCHES="${SCRIPT_DIR}/userpatches" +readonly SOURCE_LOCK="${PROJECT_USERPATCHES}/source-lock.env" +readonly FRAMEWORK_OVERRIDES="${SCRIPT_DIR}/framework-overrides" + +die() { + echo "build-image.sh: $*" >&2 + exit 1 +} + +usage() { + cat <<'EOF' +Usage: build-image.sh --ssh-public-key PATH [OPTIONS] + +Build the source-locked Armbian Debian Trixie image for Turing RK1. + +Options: + --ssh-public-key PATH Required OpenSSH public-key/authorized_keys file. + --work-dir PATH Managed framework/cache directory (default: .work). + --prepare-only Clone/stage inputs, but do not invoke Armbian. + --config-dump Stage inputs and run Armbian's non-building config dump. + -h, --help Show this help. + +The default action performs the full image build. It may ask for sudo through +Armbian's normal native-build launcher. No image is flashed by this command. +EOF +} + +validate_public_keys() { + local key_file="${1}" + [[ -f "${key_file}" ]] || die "public-key file does not exist: ${key_file}" + [[ -s "${key_file}" ]] || die "public-key file is empty: ${key_file}" + command -v ssh-keygen >/dev/null || die "ssh-keygen is required to validate the public key" + + awk ' + /^[[:space:]]*($|#)/ { next } + $1 !~ /^(ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp(256|384|521)|sk-ssh-ed25519@openssh.com|sk-ecdsa-sha2-nistp256@openssh.com)$/ { exit 1 } + { count++ } + END { if (count < 1) exit 1 } + ' "${key_file}" || die "use plain OpenSSH public keys without authorized_keys options" + ssh-keygen -l -f "${key_file}" >/dev/null || die "ssh-keygen rejected the supplied public-key file" +} + +prepare_framework() { + local work_root="${1}" + local framework_dir="${work_root}/armbian-build" + local origin + + install -d -m 0755 "${work_root}" + work_root="$(cd -- "${work_root}" && pwd -P)" + framework_dir="${work_root}/armbian-build" + [[ "${work_root}" != "/" ]] || die "refusing to use / as the work directory" + + if [[ ! -e "${framework_dir}" ]]; then + git init "${framework_dir}" >/dev/null + git -C "${framework_dir}" remote add origin "${RK1_MEDIA_ARMBIAN_BUILD_URL}" + install -m 0600 /dev/null "${framework_dir}/.rk1-media-build-root" + fi + + [[ -d "${framework_dir}/.git" ]] || die "work path is not a Git checkout: ${framework_dir}" + [[ -f "${framework_dir}/.rk1-media-build-root" ]] || die "work checkout is not managed by this script: ${framework_dir}" + origin="$(git -C "${framework_dir}" remote get-url origin)" + [[ "${origin}" == "${RK1_MEDIA_ARMBIAN_BUILD_URL}" ]] || die "unexpected Armbian origin: ${origin}" + + if git -C "${framework_dir}" rev-parse --verify HEAD >/dev/null 2>&1; then + git -C "${framework_dir}" diff --quiet || die "tracked modifications exist in ${framework_dir}" + git -C "${framework_dir}" diff --cached --quiet || die "staged modifications exist in ${framework_dir}" + fi + + if ! git -C "${framework_dir}" cat-file -e "${RK1_MEDIA_ARMBIAN_BUILD_COMMIT}^{commit}" 2>/dev/null; then + git -C "${framework_dir}" fetch --depth=1 origin "${RK1_MEDIA_ARMBIAN_BUILD_COMMIT}" + fi + git -C "${framework_dir}" checkout --detach "${RK1_MEDIA_ARMBIAN_BUILD_COMMIT}" >/dev/null + + printf '%s\n' "${framework_dir}" +} + +stage_userpatches() { + local framework_dir="${1}" + local public_key="${2}" + local destination="${framework_dir}/userpatches" + + [[ -f "${framework_dir}/.rk1-media-build-root" ]] || die "refusing to stage into an unmanaged checkout" + [[ "${destination}" == "${framework_dir}/userpatches" ]] || die "internal userpatch path check failed" + [[ ! -L "${destination}" ]] || die "refusing to replace a symlinked userpatches directory" + if [[ -e "${destination}" ]]; then + [[ -f "${destination}/.rk1-media-managed" ]] || die "existing userpatches are not managed by this script" + rm -rf -- "${destination}" + fi + + install -d -m 0755 "${destination}" + cp -a "${PROJECT_USERPATCHES}/." "${destination}/" + install -m 0600 /dev/null "${destination}/.rk1-media-managed" + install -d -m 0700 "${destination}/overlay/build-input" + install -m 0600 "${public_key}" "${destination}/overlay/build-input/authorized_keys" + install -m 0644 "${SOURCE_LOCK}" "${destination}/overlay/build-input/source-lock.env" + + # Armbian supports immutable resolutions for otherwise moving branch inputs + # through config/sources/git_sources.json. This pins its firmware artifact. + cp -a "${FRAMEWORK_OVERRIDES}/." "${framework_dir}/" +} + +main() { + local public_key="" + local work_dir="${SCRIPT_DIR}/.work" + local mode="build" + local framework_dir + + while (($#)); do + case "${1}" in + --ssh-public-key) + (($# >= 2)) || die "--ssh-public-key requires a path" + public_key="${2}" + shift 2 + ;; + --work-dir) + (($# >= 2)) || die "--work-dir requires a path" + work_dir="${2}" + shift 2 + ;; + --prepare-only) + mode="prepare" + shift + ;; + --config-dump) + mode="config-dump" + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) die "unknown option: ${1}" ;; + esac + done + + [[ -n "${public_key}" ]] || die "--ssh-public-key is required; the image never enables password login" + [[ -r "${SOURCE_LOCK}" ]] || die "source lock is missing: ${SOURCE_LOCK}" + # shellcheck source=userpatches/source-lock.env + source "${SOURCE_LOCK}" + validate_public_keys "${public_key}" + framework_dir="$(prepare_framework "${work_dir}")" + stage_userpatches "${framework_dir}" "${public_key}" + + echo "Prepared Armbian ${RK1_MEDIA_ARMBIAN_BUILD_COMMIT} in ${framework_dir}" + case "${mode}" in + prepare) + echo "Configuration staged; no build was started." + ;; + config-dump) + (cd "${framework_dir}" && CONFIG_DEFS_ONLY=yes ./compile.sh rk1-media config-dump) + ;; + build) + (cd "${framework_dir}" && ./compile.sh rk1-media build) + echo "Images and checksums: ${framework_dir}/output/images" + ;; + esac +} + +main "$@" diff --git a/armbian/framework-overrides/config/sources/git_sources.json b/armbian/framework-overrides/config/sources/git_sources.json new file mode 100644 index 0000000..96b8c06 --- /dev/null +++ b/armbian/framework-overrides/config/sources/git_sources.json @@ -0,0 +1,7 @@ +[ + { + "source": "https://github.com/armbian/firmware", + "branch": "master", + "sha1": "d9846710f54da5e4383e2d67311819659ac2cf5c" + } +] diff --git a/armbian/tests/lint.sh b/armbian/tests/lint.sh new file mode 100755 index 0000000..bcca96e --- /dev/null +++ b/armbian/tests/lint.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +readonly ARMBIAN_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)" + +bash -n \ + "${ARMBIAN_DIR}/build-image.sh" \ + "${ARMBIAN_DIR}/userpatches/config-rk1-media.conf" \ + "${ARMBIAN_DIR}/userpatches/customize-image.sh" \ + "${ARMBIAN_DIR}/userpatches/extensions/rkbin-tools.sh" \ + "${ARMBIAN_DIR}/userpatches/overlay/rootfs/usr/local/sbin/rk1-media-first-boot" + +# shellcheck source=../userpatches/source-lock.env +source "${ARMBIAN_DIR}/userpatches/source-lock.env" +for revision in \ + "${RK1_MEDIA_ARMBIAN_BUILD_COMMIT}" \ + "${RK1_MEDIA_KERNEL_COMMIT}" \ + "${RK1_MEDIA_UBOOT_COMMIT}" \ + "${RK1_MEDIA_RKBIN_COMMIT}" \ + "${RK1_MEDIA_FIRMWARE_COMMIT}"; do + [[ "${revision}" =~ ^[0-9a-f]{40}$ ]] || { echo "Invalid pinned revision: ${revision}" >&2; exit 1; } +done + +jq -e --arg revision "${RK1_MEDIA_FIRMWARE_COMMIT}" \ + '. == [{"source":"https://github.com/armbian/firmware","branch":"master","sha1":$revision}]' \ + "${ARMBIAN_DIR}/framework-overrides/config/sources/git_sources.json" >/dev/null + +[[ ! -e "${ARMBIAN_DIR}/userpatches/overlay/build-input/authorized_keys" ]] || { + echo "A staged administrator key must not be committed to userpatches/overlay/build-input" >&2 + exit 1 +} + +if command -v apt-config >/dev/null; then + apt_dump="$(apt-config -c "${ARMBIAN_DIR}/userpatches/overlay/rootfs/etc/apt/apt.conf.d/52rk1-media-unattended-upgrades" dump)" + [[ "$(grep -c '^Unattended-Upgrade::Origins-Pattern:: ' <<< "${apt_dump}")" == "1" ]] || { + echo "Unattended upgrades must contain exactly one allowed origin" >&2 + exit 1 + } + grep -q 'codename=${distro_codename}-security' <<< "${apt_dump}" || { + echo "Debian security origin is missing from unattended-upgrades policy" >&2 + exit 1 + } +fi + +if command -v shellcheck >/dev/null; then + shellcheck --exclude=SC1091,SC2154 \ + "${ARMBIAN_DIR}/build-image.sh" \ + "${ARMBIAN_DIR}/userpatches/config-rk1-media.conf" \ + "${ARMBIAN_DIR}/userpatches/customize-image.sh" \ + "${ARMBIAN_DIR}/userpatches/extensions/rkbin-tools.sh" \ + "${ARMBIAN_DIR}/userpatches/overlay/rootfs/usr/local/sbin/rk1-media-first-boot" +fi + +echo "Armbian scaffolding checks passed." diff --git a/armbian/userpatches/VERSION b/armbian/userpatches/VERSION new file mode 100644 index 0000000..49b49e4 --- /dev/null +++ b/armbian/userpatches/VERSION @@ -0,0 +1,2 @@ +0.1.0 + diff --git a/armbian/userpatches/config-rk1-media.conf b/armbian/userpatches/config-rk1-media.conf new file mode 100644 index 0000000..4274761 --- /dev/null +++ b/armbian/userpatches/config-rk1-media.conf @@ -0,0 +1,72 @@ +# shellcheck shell=bash + +# Armbian image identity and target. +BOARD="turing-rk1" +BRANCH="vendor" +RELEASE="trixie" +BUILD_MINIMAL="yes" +BUILD_DESKTOP="no" +KERNEL_CONFIGURE="no" +UBOOT_CONFIGURE="no" +INSTALL_HEADERS="no" +EXPERT="yes" # Turing RK1 is an Armbian community-supported board. + +# Image layout and deterministic build choices. +ROOTFS_TYPE="ext4" +EXTRA_ROOTFS_MIB_SIZE="1536" +COMPRESS_OUTPUTIMAGE="sha,xz" +IMAGE_XZ_COMPRESSION_RATIO="6" +NETWORKING_STACK="systemd-networkd" +SHARE_LOGS="no" +BETA="no" +USE_CCACHE="no" + +# Runtime defaults. The temporary build password is overwritten with an +# unusable shadow value by customize-image.sh; no password works in the image. +HOST="rk1-media" +VENDOR="RK1 Media" +VENDORURL="https://github.com/armbian/build" +ROOTPWD="RK1_IMAGE_LOGIN_DISABLED" +CONSOLE_AUTOLOGIN="no" +DESKTOP_AUTOLOGIN="no" +OPENSSHD_REGENERATE_HOST_KEYS="false" + +# This file is copied with the rest of userpatches by build-image.sh. +# shellcheck source=source-lock.env +source "${BASH_SOURCE%/*}/source-lock.env" + +# The board and family definitions intentionally use moving branches. Override +# them after those definitions have run while retaining Armbian's matching +# vendor patch directories. +function post_family_config__999_rk1_media_pin_boot_sources() { + [[ "${BOARD}" == "turing-rk1" ]] || exit_with_error "RK1 media config used with the wrong board" "${BOARD}" + [[ "${BRANCH}" == "vendor" ]] || exit_with_error "RK1 media config requires BRANCH=vendor" "${BRANCH}" + + declare -g KERNELSOURCE="${RK1_MEDIA_KERNEL_URL}" + declare -g KERNELBRANCH="commit:${RK1_MEDIA_KERNEL_COMMIT}" + declare -g KERNEL_MAJOR_MINOR="${RK1_MEDIA_KERNEL_SERIES}" + declare -g KERNELPATCHDIR="${RK1_MEDIA_KERNEL_PATCHSET}" + + declare -g BOOTSOURCE="${RK1_MEDIA_UBOOT_URL}" + declare -g BOOTBRANCH="commit:${RK1_MEDIA_UBOOT_COMMIT}" + declare -g BOOTPATCHDIR="${RK1_MEDIA_UBOOT_PATCHSET}" +} + +function user_config__200_rk1_media_base_packages() { + [[ "${RELEASE}" == "trixie" ]] || exit_with_error "RK1 media config requires Debian Trixie" "${RELEASE}" + + add_packages_to_image \ + openssh-server \ + sudo \ + avahi-daemon \ + libnss-mdns \ + unattended-upgrades \ + ca-certificates \ + curl \ + jq + + # Do not inherit the build host's locale/time zone. + declare -g DEST_LANG="en_US.UTF-8" + declare -g TZDATA="Etc/UTC" +} + diff --git a/armbian/userpatches/customize-image.sh b/armbian/userpatches/customize-image.sh new file mode 100755 index 0000000..33eb5ff --- /dev/null +++ b/armbian/userpatches/customize-image.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +RELEASE="${1:?missing Armbian release}" +LINUXFAMILY="${2:?missing Linux family}" +BOARD="${3:?missing board}" +BUILD_DESKTOP="${4:?missing desktop flag}" +ARCH="${5:?missing architecture}" + +readonly RELEASE LINUXFAMILY BOARD BUILD_DESKTOP ARCH +readonly OVERLAY_ROOT="/tmp/overlay/rootfs" +readonly AUTHORIZED_KEYS_INPUT="/tmp/overlay/build-input/authorized_keys" +readonly SOURCE_LOCK_INPUT="/tmp/overlay/build-input/source-lock.env" + +fail() { + echo "rk1-media customize: $*" >&2 + exit 1 +} + +configure_identity() { + printf '%s\n' "rk1-media" > /etc/hostname + if grep -Eq '^127\.0\.1\.1[[:space:]]' /etc/hosts; then + sed -Ei 's/^127\.0\.1\.1[[:space:]].*$/127.0.1.1 rk1-media/' /etc/hosts + else + printf '%s\n' '127.0.1.1 rk1-media' >> /etc/hosts + fi +} + +configure_boot() { + local env_file="/boot/armbianEnv.txt" + local overlays + [[ -f "${env_file}" ]] || fail "Armbian boot environment is missing: ${env_file}" + + if grep -q '^overlays=' "${env_file}"; then + overlays="$(sed -n 's/^overlays=//p' "${env_file}" | head -n 1)" + case " ${overlays} " in + *" panthor-gpu "*) ;; + *) sed -i "0,/^overlays=.*/s//overlays=${overlays} panthor-gpu/" "${env_file}" ;; + esac + else + printf '%s\n' 'overlays=panthor-gpu' >> "${env_file}" + fi +} + +configure_admin() { + if ! getent group rkadmin >/dev/null; then + groupadd rkadmin + fi + if ! id rkadmin >/dev/null 2>&1; then + useradd --create-home --gid rkadmin --shell /bin/bash --comment "RK1 administrator" rkadmin + else + usermod --gid rkadmin rkadmin + fi + + local group groups_csv + local -a groups=(sudo adm systemd-journal audio video render input) + local -a available_groups=() + for group in "${groups[@]}"; do + getent group "${group}" >/dev/null && available_groups+=("${group}") + done + groups_csv="$(IFS=,; echo "${available_groups[*]}")" + [[ -z "${groups_csv}" ]] || usermod --append --groups "${groups_csv}" rkadmin + + install -d -o rkadmin -g rkadmin -m 0700 /home/rkadmin/.ssh + install -o rkadmin -g rkadmin -m 0600 "${AUTHORIZED_KEYS_INPUT}" /home/rkadmin/.ssh/authorized_keys + install -m 0440 /dev/null /etc/sudoers.d/90-rkadmin + printf '%s\n' 'rkadmin ALL=(ALL:ALL) NOPASSWD: ALL' > /etc/sudoers.d/90-rkadmin + visudo --check --file=/etc/sudoers.d/90-rkadmin >/dev/null + + # Replace (rather than merely prefix) the build-time password hashes. + usermod --password '!' root + usermod --password '!' rkadmin + rm -f /root/.not_logged_in_yet +} + +hold_platform_packages() { + local package + local -a held=() + while IFS= read -r package; do + case "${package}" in + linux-image-*|linux-dtb-*|linux-u-boot-*|armbian-bsp-*) + apt-mark hold "${package}" >/dev/null + held+=("${package}") + ;; + esac + done < <(dpkg-query --show --showformat='${binary:Package}\n') + printf '%s\n' "${held[@]}" | LC_ALL=C sort -u > /usr/share/rk1-media/held-packages.txt +} + +configure_services() { + install -d -m 0755 /var/lib/rk1-media + install -m 0600 /dev/null /var/lib/rk1-media/first-boot.pending + + # Validate the complete SSH configuration, then remove generated host keys so + # every flashed module creates a unique set before ssh.service can start. + ssh-keygen -A + /usr/sbin/sshd -t + rm -f /etc/ssh/ssh_host_* + + systemctl enable rk1-media-first-boot.service + systemctl enable ssh.service + systemctl enable avahi-daemon.service + systemctl enable apt-daily.timer + systemctl enable apt-daily-upgrade.timer +} + +write_build_manifest() { + install -d -m 0755 /usr/share/rk1-media + install -m 0644 "${SOURCE_LOCK_INPUT}" /usr/share/rk1-media/source-lock.env + dpkg-query --show --showformat='${binary:Package}\t${Version}\t${Architecture}\n' \ + | LC_ALL=C sort > /usr/share/rk1-media/package-manifest.tsv +} + +main() { + [[ "${RELEASE}" == "trixie" ]] || fail "unexpected release: ${RELEASE}" + [[ "${BOARD}" == "turing-rk1" ]] || fail "unexpected board: ${BOARD}" + [[ "${BUILD_DESKTOP}" == "no" ]] || fail "desktop images are not supported by this customization" + [[ "${ARCH}" == "arm64" ]] || fail "unexpected architecture: ${ARCH}" + [[ -d "${OVERLAY_ROOT}" ]] || fail "rootfs overlay is missing" + [[ -s "${AUTHORIZED_KEYS_INPUT}" ]] || fail "authorized_keys build input is missing" + [[ -s "${SOURCE_LOCK_INPUT}" ]] || fail "source lock build input is missing" + + cp -a "${OVERLAY_ROOT}/." / + configure_identity + configure_boot + configure_admin + write_build_manifest + hold_platform_packages + configure_services + + # systemd creates a unique machine ID on the first real boot. + : > /etc/machine-id + rm -f /var/lib/dbus/machine-id +} + +main "$@" diff --git a/armbian/userpatches/extensions/rkbin-tools.sh b/armbian/userpatches/extensions/rkbin-tools.sh new file mode 100755 index 0000000..b4e06cf --- /dev/null +++ b/armbian/userpatches/extensions/rkbin-tools.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# Source-locked replacement for Armbian's rkbin-tools extension. The upstream +# extension follows branch:master, which makes otherwise pinned builds drift. + +function fetch_sources_tools__rkbin_tools() { + : "${RK1_MEDIA_RKBIN_URL:?source-lock.env did not set RK1_MEDIA_RKBIN_URL}" + : "${RK1_MEDIA_RKBIN_COMMIT:?source-lock.env did not set RK1_MEDIA_RKBIN_COMMIT}" + fetch_from_repo "${RK1_MEDIA_RKBIN_URL}" "rkbin-tools" "commit:${RK1_MEDIA_RKBIN_COMMIT}" +} + +function build_host_tools__install_rkbin_tools() { + cd "${SRC}/cache/sources/rkbin-tools" || exit + if [[ ! -f .commit_id || "$(improved_git rev-parse @ 2>/dev/null)" != "$(< .commit_id)" || ! -f /usr/local/bin/loaderimage ]]; then + display_alert "Installing" "source-locked rkbin tools" "info" + mkdir -p /usr/local/bin/ + install -m 0755 tools/loaderimage /usr/local/bin/ + install -m 0755 tools/trust_merger /usr/local/bin/ + improved_git rev-parse @ 2>/dev/null > .commit_id + fi +} + diff --git a/armbian/userpatches/overlay/rootfs/etc/apt/apt.conf.d/20auto-upgrades b/armbian/userpatches/overlay/rootfs/etc/apt/apt.conf.d/20auto-upgrades new file mode 100644 index 0000000..f936ebe --- /dev/null +++ b/armbian/userpatches/overlay/rootfs/etc/apt/apt.conf.d/20auto-upgrades @@ -0,0 +1,3 @@ +APT::Periodic::Update-Package-Lists "1"; +APT::Periodic::Unattended-Upgrade "1"; + diff --git a/armbian/userpatches/overlay/rootfs/etc/apt/apt.conf.d/52rk1-media-unattended-upgrades b/armbian/userpatches/overlay/rootfs/etc/apt/apt.conf.d/52rk1-media-unattended-upgrades new file mode 100644 index 0000000..c767ed4 --- /dev/null +++ b/armbian/userpatches/overlay/rootfs/etc/apt/apt.conf.d/52rk1-media-unattended-upgrades @@ -0,0 +1,22 @@ +// Security fixes may install automatically. Platform/media packages remain +// pinned until a rebuilt image passes the RK1 hardware acceptance suite. +#clear Unattended-Upgrade::Allowed-Origins; +#clear Unattended-Upgrade::Origins-Pattern; + +Unattended-Upgrade::Origins-Pattern { + "origin=Debian,codename=${distro_codename}-security,label=Debian-Security"; +}; + +Unattended-Upgrade::Package-Blacklist { + "^linux-image-.*$"; + "^linux-dtb-.*$"; + "^linux-u-boot-.*$"; + "^armbian-bsp-.*$"; + "^mesa-.*$"; + "^kodi.*$"; + "^rknn.*$"; + "^rockchip.*$"; +}; + +Unattended-Upgrade::Automatic-Reboot "false"; +Unattended-Upgrade::Remove-Unused-Kernel-Packages "false"; diff --git a/armbian/userpatches/overlay/rootfs/etc/ssh/sshd_config.d/10-rk1-media.conf b/armbian/userpatches/overlay/rootfs/etc/ssh/sshd_config.d/10-rk1-media.conf new file mode 100644 index 0000000..a87feec --- /dev/null +++ b/armbian/userpatches/overlay/rootfs/etc/ssh/sshd_config.d/10-rk1-media.conf @@ -0,0 +1,8 @@ +# Generated by rk1-media-image. Administrative access is SSH-key-only. +PermitRootLogin no +PasswordAuthentication no +KbdInteractiveAuthentication no +PubkeyAuthentication yes +AuthenticationMethods publickey +PermitEmptyPasswords no + diff --git a/armbian/userpatches/overlay/rootfs/etc/systemd/system/rk1-media-first-boot.service b/armbian/userpatches/overlay/rootfs/etc/systemd/system/rk1-media-first-boot.service new file mode 100644 index 0000000..18a187c --- /dev/null +++ b/armbian/userpatches/overlay/rootfs/etc/systemd/system/rk1-media-first-boot.service @@ -0,0 +1,14 @@ +[Unit] +Description=RK1 media image first-boot identity setup +After=local-fs.target +Before=ssh.service sshd.service +ConditionPathExists=/var/lib/rk1-media/first-boot.pending + +[Service] +Type=oneshot +ExecStart=/usr/local/sbin/rk1-media-first-boot +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target + diff --git a/armbian/userpatches/overlay/rootfs/etc/systemd/system/ssh.service.d/10-rk1-media-first-boot.conf b/armbian/userpatches/overlay/rootfs/etc/systemd/system/ssh.service.d/10-rk1-media-first-boot.conf new file mode 100644 index 0000000..1e2701a --- /dev/null +++ b/armbian/userpatches/overlay/rootfs/etc/systemd/system/ssh.service.d/10-rk1-media-first-boot.conf @@ -0,0 +1,4 @@ +[Unit] +Requires=rk1-media-first-boot.service +After=rk1-media-first-boot.service + diff --git a/armbian/userpatches/overlay/rootfs/usr/local/sbin/rk1-media-first-boot b/armbian/userpatches/overlay/rootfs/usr/local/sbin/rk1-media-first-boot new file mode 100755 index 0000000..332bbb3 --- /dev/null +++ b/armbian/userpatches/overlay/rootfs/usr/local/sbin/rk1-media-first-boot @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +umask 077 + +readonly MARKER="/var/lib/rk1-media/first-boot.pending" +[[ -e "${MARKER}" ]] || exit 0 + +rm -f /etc/ssh/ssh_host_* +ssh-keygen -A + +found_key="no" +for private_key in /etc/ssh/ssh_host_*_key; do + if [[ -s "${private_key}" ]]; then + found_key="yes" + break + fi +done +[[ "${found_key}" == "yes" ]] || { echo "No SSH host key was generated" >&2; exit 1; } + +/usr/sbin/sshd -t +rm -f "${MARKER}" +sync + diff --git a/armbian/userpatches/source-lock.env b/armbian/userpatches/source-lock.env new file mode 100644 index 0000000..da75530 --- /dev/null +++ b/armbian/userpatches/source-lock.env @@ -0,0 +1,23 @@ +# Source revisions resolved on 2026-08-16. +# +# This file is sourced by both build-image.sh and Armbian's user configuration. +# Keep values shell-safe (no command substitutions or unquoted whitespace). + +RK1_MEDIA_ARMBIAN_BUILD_URL="https://github.com/armbian/build.git" +RK1_MEDIA_ARMBIAN_BUILD_COMMIT="ea18947bed789c829a260df129e815c353c14908" + +RK1_MEDIA_KERNEL_URL="https://github.com/armbian/linux-rockchip.git" +RK1_MEDIA_KERNEL_COMMIT="5280f9b4336199c4025c8eed894d2b4e2268dcc6" +RK1_MEDIA_KERNEL_VERSION="6.1.115" +RK1_MEDIA_KERNEL_SERIES="6.1" +RK1_MEDIA_KERNEL_PATCHSET="rk35xx-vendor-6.1" + +RK1_MEDIA_UBOOT_URL="https://github.com/u-boot/u-boot.git" +RK1_MEDIA_UBOOT_COMMIT="ece349ade2973e220f524ce59e59711cc919263f" +RK1_MEDIA_UBOOT_PATCHSET="v2026.07" + +RK1_MEDIA_RKBIN_URL="https://github.com/armbian/rkbin.git" +RK1_MEDIA_RKBIN_COMMIT="452f49a987097d7ca675811e51a873b42ebd101f" + +RK1_MEDIA_FIRMWARE_URL="https://github.com/armbian/firmware" +RK1_MEDIA_FIRMWARE_COMMIT="d9846710f54da5e4383e2d67311819659ac2cf5c" diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..af94f65 --- /dev/null +++ b/build.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Assemble the complete RK1 media-center image from locked binary inputs. + +set -Eeuo pipefail +umask 022 + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) +SSH_PUBLIC_KEY="" +IMAGE_SIZE_MIB=4096 +IMAGE_REVISION="r4" +FORCE_REPACK=0 + +usage() { + cat <<'EOF' +Usage: ./build.sh --ssh-public-key PATH [--image-size-mib N] [--revision NAME] [--force-repack] + +Builds a flashable Turing RK1 image without root, loop devices, or mounts. +The key file may contain one or more plain OpenSSH public keys. +EOF +} + +while (($#)); do + case "$1" in + --ssh-public-key) SSH_PUBLIC_KEY=${2:?}; shift 2 ;; + --image-size-mib) IMAGE_SIZE_MIB=${2:?}; shift 2 ;; + --revision) IMAGE_REVISION=${2:?}; shift 2 ;; + --force-repack) FORCE_REPACK=1; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown argument: $1" >&2; usage >&2; exit 2 ;; + esac +done +[[ -n "$SSH_PUBLIC_KEY" ]] || { usage >&2; exit 2; } + +"$SCRIPT_DIR/scripts/fetch-inputs.sh" + +upstream_deb="$SCRIPT_DIR/downloads/kodi-rockchip-gbm_20260721-1741_trixie_arm64.deb" +media_deb="$SCRIPT_DIR/media/out/rk1-media-stack_20260721.1741+rk1.1_arm64.deb" +if ((FORCE_REPACK)) || [[ ! -s "$media_deb" ]]; then + "$SCRIPT_DIR/media/scripts/repack-upstream-deb.sh" \ + --deb "$upstream_deb" --output-dir "$SCRIPT_DIR/media/out" +fi +[[ -s "$media_deb.sha256" ]] || { echo "Missing media checksum" >&2; exit 1; } +( + cd "$(dirname -- "$media_deb")" + sha256sum --check --status "$(basename -- "$media_deb.sha256")" +) || { echo "Repacked media checksum failed" >&2; exit 1; } + +"$SCRIPT_DIR/image/build-image.sh" \ + --ssh-public-key "$SSH_PUBLIC_KEY" \ + --kodi-deb "$media_deb" \ + --image-size-mib "$IMAGE_SIZE_MIB" \ + --revision "$IMAGE_REVISION" diff --git a/image/build-image.sh b/image/build-image.sh new file mode 100755 index 0000000..30fccb0 --- /dev/null +++ b/image/build-image.sh @@ -0,0 +1,325 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +umask 022 + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +PROJECT_DIR=$(cd -- "${SCRIPT_DIR}/.." && pwd) + +BASE_XZ="${PROJECT_DIR}/downloads/Armbian_community_26.11.0-trunk.1_Turing-rk1_trixie_vendor_6.1.115_minimal.img.xz" +BASE_SHA256="cbbd16d90786331fbfbbb695614a53f5e87ea441ea37b425d986334318190cf6" +KODI_DEB="${PROJECT_DIR}/media/out/rk1-media-stack_20260721.1741+rk1.1_arm64.deb" +DEB_DIR="${PROJECT_DIR}/work/apt/archives" +RUNTIME_DIR="${PROJECT_DIR}/runtime" +RKNN_SOURCE_DIR="${PROJECT_DIR}/downloads/rknn-toolkit2-v2.3.2-minimal" +SSH_PUBLIC_KEY="" +OUTPUT_DIR="${PROJECT_DIR}/dist" +IMAGE_SIZE_MIB=4096 +IMAGE_REVISION="r4" + +usage() { + cat <<'EOF' +Usage: build-image.sh --ssh-public-key PATH [options] + +Options: + --base-xz PATH Verified Armbian .img.xz base + --kodi-deb PATH Repacked RKMPP Kodi arm64 package + --deb-dir PATH Offline Debian dependency directory + --runtime-dir PATH RKNN/self-test runtime project + --rknn-source-dir PATH Offline RKNN Toolkit2 v2.3.2 asset bundle + --output-dir PATH Artifact directory + --image-size-mib N Expanded image size (default: 4096) + --revision NAME Artifact revision suffix (default: r4) +EOF +} + +while (($#)); do + case "$1" in + --ssh-public-key) SSH_PUBLIC_KEY=${2:?}; shift 2 ;; + --base-xz) BASE_XZ=${2:?}; shift 2 ;; + --kodi-deb) KODI_DEB=${2:?}; shift 2 ;; + --deb-dir) DEB_DIR=${2:?}; shift 2 ;; + --runtime-dir) RUNTIME_DIR=${2:?}; shift 2 ;; + --rknn-source-dir) RKNN_SOURCE_DIR=${2:?}; shift 2 ;; + --output-dir) OUTPUT_DIR=${2:?}; shift 2 ;; + --image-size-mib) IMAGE_SIZE_MIB=${2:?}; shift 2 ;; + --revision) IMAGE_REVISION=${2:?}; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown argument: $1" >&2; usage >&2; exit 2 ;; + esac +done + +for command_name in debugfs e2fsck install jq resize2fs sfdisk sgdisk sha256sum ssh-keygen xz; do + command -v "$command_name" >/dev/null || { echo "Missing command: $command_name" >&2; exit 1; } +done +[[ -n "$SSH_PUBLIC_KEY" && -s "$SSH_PUBLIC_KEY" ]] || { + echo "--ssh-public-key must name a non-empty public-key file" >&2 + exit 2 +} +grep -Eq -- '-----BEGIN .*PRIVATE KEY-----' "$SSH_PUBLIC_KEY" && { + echo "Refusing a private key: $SSH_PUBLIC_KEY" >&2 + exit 2 +} +awk ' + /^[[:space:]]*($|#)/ { next } + $1 !~ /^(ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp(256|384|521)|sk-ssh-ed25519@openssh.com|sk-ecdsa-sha2-nistp256@openssh.com)$/ { exit 1 } + NF < 2 { exit 1 } + { count++ } + END { if (count < 1) exit 1 } +' "$SSH_PUBLIC_KEY" || { + echo "Use plain OpenSSH public-key lines without authorized_keys options" >&2 + exit 2 +} +ssh-keygen -lf "$SSH_PUBLIC_KEY" >/dev/null || { + echo "ssh-keygen rejected the public-key file" >&2 + exit 2 +} +[[ -s "$BASE_XZ" ]] || { echo "Missing base image: $BASE_XZ" >&2; exit 1; } +[[ -s "$KODI_DEB" ]] || { echo "Missing Kodi package: $KODI_DEB" >&2; exit 1; } +[[ -d "$DEB_DIR" ]] || { echo "Missing dependency directory: $DEB_DIR" >&2; exit 1; } +[[ -x "$RUNTIME_DIR/install-rknn-runtime.sh" ]] || { + echo "Missing RKNN installer below: $RUNTIME_DIR" >&2 + exit 1 +} +[[ -d "$RKNN_SOURCE_DIR" ]] || { + echo "Missing offline RKNN source bundle: $RKNN_SOURCE_DIR" >&2 + exit 1 +} +[[ "$IMAGE_SIZE_MIB" =~ ^[0-9]+$ ]] && ((IMAGE_SIZE_MIB >= 3072)) || { + echo "Image size must be an integer of at least 3072 MiB" >&2 + exit 2 +} +[[ "$IMAGE_REVISION" =~ ^[a-z0-9][a-z0-9._-]*$ ]] || { + echo "Image revision contains unsafe characters: $IMAGE_REVISION" >&2 + exit 2 +} + +actual_base_sha=$(sha256sum "$BASE_XZ" | awk '{print $1}') +[[ "$actual_base_sha" == "$BASE_SHA256" ]] || { + echo "Base image checksum mismatch: $actual_base_sha" >&2 + exit 1 +} + +repair_ext4() { + local status + if e2fsck -fy "$1" >/dev/null; then + return 0 + else + status=$? + fi + # fsck bit 0 means errors were corrected; this is expected after debugfs. + ((status == 1)) || { + echo "e2fsck failed for $1 with status $status" >&2 + return "$status" + } +} + +BUILD_ID="$(date -u +%Y%m%d)-${IMAGE_REVISION}-trixie-vendor-6.1.115" +WORK_DIR="${PROJECT_DIR}/work/image-${BUILD_ID}" +STAGE_DIR="${WORK_DIR}/overlay" +RAW_IMAGE="${WORK_DIR}/rk1-media-${BUILD_ID}.img" +ROOTFS_IMAGE="${WORK_DIR}/rootfs.ext4" +OUTPUT_BASENAME="rk1-media-${BUILD_ID}-turing-rk1-emmc" + +rm -rf -- "$WORK_DIR" +mkdir -p "$STAGE_DIR" "$OUTPUT_DIR" +cp -a "${SCRIPT_DIR}/overlay/." "$STAGE_DIR/" +mkdir -p "$STAGE_DIR/opt/rk1-seed/debs" "$STAGE_DIR/usr/share/doc/rk1-media-image" +install -D -m 0755 "$PROJECT_DIR/scripts/live-migrate-root-to-nvme.sh" \ + "$STAGE_DIR/usr/local/sbin/rk1-media-migrate-root-to-nvme" +cp -a "$DEB_DIR"/*.deb "$STAGE_DIR/opt/rk1-seed/debs/" +cp -a "$KODI_DEB" "$STAGE_DIR/opt/rk1-seed/debs/" +cp -a "$SSH_PUBLIC_KEY" "$STAGE_DIR/opt/rk1-seed/authorized_keys" +( + cd "$STAGE_DIR/opt/rk1-seed/debs" + sha256sum -- *.deb >../debs.sha256 +) +cp -a "$RUNTIME_DIR/README.md" "$RUNTIME_DIR/rknn-version.env" \ + "$STAGE_DIR/usr/share/doc/rk1-media-image/" +"$RUNTIME_DIR/install-rknn-runtime.sh" \ + --rootfs "$STAGE_DIR" --source-dir "$RKNN_SOURCE_DIR" + +xz --decompress --stdout "$BASE_XZ" >"$RAW_IMAGE" +partition_json=$(sfdisk --json "$RAW_IMAGE") +partition_start=$(jq -r '.partitiontable.partitions[0].start' <<<"$partition_json") +partition_size=$(jq -r '.partitiontable.partitions[0].size' <<<"$partition_json") +partition_type=$(jq -r '.partitiontable.partitions[0].type' <<<"$partition_json") +partition_uuid=$(jq -r '.partitiontable.partitions[0].uuid' <<<"$partition_json") +[[ "$partition_start" == "32768" ]] || { + echo "Unexpected base partition start: $partition_start" >&2 + exit 1 +} + +dd if="$RAW_IMAGE" of="$ROOTFS_IMAGE" bs=512 skip="$partition_start" count="$partition_size" status=none +repair_ext4 "$ROOTFS_IMAGE" + +firstrun_file="${WORK_DIR}/armbian-firstrun" +debugfs -R 'cat /etc/default/armbian-firstrun' "$ROOTFS_IMAGE" \ + 2>/dev/null >"$firstrun_file" +if grep -q '^OPENSSHD_REGENERATE_HOST_KEYS=' "$firstrun_file"; then + sed -i 's/^OPENSSHD_REGENERATE_HOST_KEYS=.*/OPENSSHD_REGENERATE_HOST_KEYS=false/' \ + "$firstrun_file" +else + printf '\nOPENSSHD_REGENERATE_HOST_KEYS=false\n' >>"$firstrun_file" +fi +mkdir -p "$STAGE_DIR/etc/default" +cp "$firstrun_file" "$STAGE_DIR/etc/default/armbian-firstrun" + +# Remove the generic base image's root password hash before the image exists. +shadow_file="${WORK_DIR}/shadow" +debugfs -R 'cat /etc/shadow' "$ROOTFS_IMAGE" 2>/dev/null \ + | awk 'BEGIN { FS=OFS=":" } $1 == "root" { $2="!"; found=1 } { print } END { if (!found) exit 1 }' \ + >"$shadow_file" +mkdir -p "$STAGE_DIR/etc" +cp "$shadow_file" "$STAGE_DIR/etc/shadow" + +truncate -s "${IMAGE_SIZE_MIB}M" "$RAW_IMAGE" +sgdisk --move-second-header "$RAW_IMAGE" >/dev/null +sgdisk --delete=1 "$RAW_IMAGE" >/dev/null +sgdisk --new="1:${partition_start}:0" --typecode="1:${partition_type}" --change-name='1:rootfs' "$RAW_IMAGE" >/dev/null +sgdisk --partition-guid="1:${partition_uuid}" "$RAW_IMAGE" >/dev/null + +partition_json=$(sfdisk --json "$RAW_IMAGE") +new_partition_size=$(jq -r '.partitiontable.partitions[0].size' <<<"$partition_json") +truncate -s "$((new_partition_size * 512))" "$ROOTFS_IMAGE" +resize2fs "$ROOTFS_IMAGE" >/dev/null + +env_file="${WORK_DIR}/armbianEnv.txt" +debugfs -R 'cat /boot/armbianEnv.txt' "$ROOTFS_IMAGE" 2>/dev/null >"$env_file" +if sed -n 's/^overlays=//p' "$env_file" | tr '[:space:]' '\n' | grep -Fxq panthor-gpu; then + : +elif grep -q '^overlays=' "$env_file"; then + sed -i '/^overlays=/ s/$/ panthor-gpu/' "$env_file" +else + printf '\noverlays=panthor-gpu\n' >>"$env_file" +fi +mkdir -p "$STAGE_DIR/boot" +cp "$env_file" "$STAGE_DIR/boot/armbianEnv.txt" + +# Host umasks must not leak group-writable modes into /etc or systemd paths. +find "$STAGE_DIR" -type d -exec chmod 0755 {} + +find "$STAGE_DIR" -type f ! -perm /0111 -exec chmod 0644 {} + +find "$STAGE_DIR" -type f -perm /0111 -exec chmod 0755 {} + +chmod 0640 "$STAGE_DIR/etc/shadow" + +"${SCRIPT_DIR}/inject-ext4-tree.sh" "$ROOTFS_IMAGE" "$STAGE_DIR" +debugfs -w -R 'set_inode_field /etc/shadow gid 42' "$ROOTFS_IMAGE" >/dev/null +debugfs -w -R 'set_inode_field /etc/shadow mode 0100640' "$ROOTFS_IMAGE" >/dev/null + +# Replace the overlay's portable placeholder files with real enablement links. +debugfs -w -R 'rm /etc/systemd/system/sysinit.target.wants/rk1-media-identity.service' "$ROOTFS_IMAGE" >/dev/null 2>&1 || true +debugfs -w -R 'symlink /etc/systemd/system/sysinit.target.wants/rk1-media-identity.service ../rk1-media-identity.service' "$ROOTFS_IMAGE" >/dev/null +debugfs -w -R 'rm /etc/systemd/system/multi-user.target.wants/rk1-media-provision.service' "$ROOTFS_IMAGE" >/dev/null 2>&1 || true +debugfs -w -R 'symlink /etc/systemd/system/multi-user.target.wants/rk1-media-provision.service ../rk1-media-provision.service' "$ROOTFS_IMAGE" >/dev/null + +for stale_path in \ + /root/.not_logged_in_yet \ + /root/.ssh/authorized_keys \ + /etc/ssh/ssh_host_ecdsa_key /etc/ssh/ssh_host_ecdsa_key.pub \ + /etc/ssh/ssh_host_ed25519_key /etc/ssh/ssh_host_ed25519_key.pub \ + /etc/ssh/ssh_host_rsa_key /etc/ssh/ssh_host_rsa_key.pub; do + debugfs -w -R "rm ${stale_path}" "$ROOTFS_IMAGE" >/dev/null 2>&1 || true +done + +ext4_stat() { + debugfs -R "stat $1" "$ROOTFS_IMAGE" 2>/dev/null +} +ext4_exists() { + ext4_stat "$1" | grep -q '^Inode:' +} +ext4_missing() { + ! ext4_exists "$1" +} +assert_ext4_contains() { + local path=$1 pattern=$2 + debugfs -R "cat $path" "$ROOTFS_IMAGE" 2>/dev/null | grep -Eq "$pattern" || { + echo "Image assertion failed for $path: $pattern" >&2 + exit 1 + } +} + +assert_ext4_contains /boot/armbianEnv.txt '^fdtfile=rockchip/rk3588-turing-rk1\.dtb$' +assert_ext4_contains /boot/armbianEnv.txt '^overlays=.*panthor-gpu' +assert_ext4_contains /etc/default/armbian-firstrun '^OPENSSHD_REGENERATE_HOST_KEYS=false$' +assert_ext4_contains /etc/ssh/sshd_config.d/90-rk1-media.conf '^AuthenticationMethods publickey$' +assert_ext4_contains /etc/shadow '^root:!:' +assert_ext4_contains /usr/local/sbin/rk1-media-provision \ + '^seed_archives=/opt/rk1-seed/debs$' +assert_ext4_contains /usr/local/sbin/rk1-media-provision \ + 'Dir::Cache::archives=.*seed_archives' +assert_ext4_contains /usr/local/sbin/rk1-media-provision \ + '^systemctl --no-block start .*kodi-rk\.service$' +assert_ext4_contains /usr/local/sbin/rk1-media-provision \ + '^udevadm trigger --action=add --subsystem-match=misc' +ext4_exists /opt/rknn/2.3.2/lib/librknnrt.so || { echo 'RKNN runtime is absent' >&2; exit 1; } +ext4_exists /usr/local/sbin/rk1-media-migrate-root-to-nvme || { + echo 'Guarded NVMe migration helper is absent' >&2 + exit 1 +} +ext4_exists /opt/rk1-seed/debs/"$(basename -- "$KODI_DEB")" || { echo 'Kodi package is absent' >&2; exit 1; } +expected_deb_count=$(find "$STAGE_DIR/opt/rk1-seed/debs" -maxdepth 1 -type f -name '*.deb' | wc -l) +actual_deb_count=$(debugfs -R 'ls -p /opt/rk1-seed/debs' "$ROOTFS_IMAGE" 2>/dev/null | grep -c '/.*\.deb/' || true) +[[ "$actual_deb_count" == "$expected_deb_count" ]] || { + echo "Offline package bundle is incomplete ($actual_deb_count/$expected_deb_count)" >&2 + exit 1 +} +ext4_stat /etc | grep -Eq 'Mode:[[:space:]]+0755' || { echo '/etc mode is unsafe' >&2; exit 1; } +ext4_stat /etc/systemd/system/sysinit.target.wants/rk1-media-identity.service \ + | grep -Fq 'Fast link dest: "../rk1-media-identity.service"' || { echo 'Identity service is not enabled' >&2; exit 1; } +ext4_stat /etc/systemd/system/multi-user.target.wants/rk1-media-provision.service \ + | grep -Fq 'Fast link dest: "../rk1-media-provision.service"' || { echo 'Provisioning service is not enabled' >&2; exit 1; } +for forbidden_path in /root/.not_logged_in_yet /root/.ssh/authorized_keys \ + /etc/ssh/ssh_host_ecdsa_key /etc/ssh/ssh_host_ed25519_key /etc/ssh/ssh_host_rsa_key; do + ext4_missing "$forbidden_path" || { echo "Forbidden image-time credential: $forbidden_path" >&2; exit 1; } +done + +repair_ext4 "$ROOTFS_IMAGE" +dd if="$ROOTFS_IMAGE" of="$RAW_IMAGE" bs=512 seek="$partition_start" conv=notrunc status=none +sgdisk --verify "$RAW_IMAGE" >/dev/null + +cp --reflink=auto "$RAW_IMAGE" "${OUTPUT_DIR}/${OUTPUT_BASENAME}.img" +xz --threads=0 -3 --keep --force "${OUTPUT_DIR}/${OUTPUT_BASENAME}.img" +( + cd "$OUTPUT_DIR" + sha256sum "${OUTPUT_BASENAME}.img.xz" >"${OUTPUT_BASENAME}.img.xz.sha256" +) + +raw_sha=$(sha256sum "${OUTPUT_DIR}/${OUTPUT_BASENAME}.img" | awk '{print $1}') +compressed_sha=$(sha256sum "${OUTPUT_DIR}/${OUTPUT_BASENAME}.img.xz" | awk '{print $1}') +fingerprints_json=$(ssh-keygen -lf "$SSH_PUBLIC_KEY" | awk '{print $2}' \ + | jq -Rsc 'split("\n") | map(select(length > 0))') +jq -n \ + --arg build_id "$BUILD_ID" \ + --arg base_file "$(basename -- "$BASE_XZ")" \ + --arg base_sha256 "$actual_base_sha" \ + --arg media_deb "$(basename -- "$KODI_DEB")" \ + --arg media_deb_sha256 "$(sha256sum "$KODI_DEB" | awk '{print $1}')" \ + --arg rknn_version 2.3.2 \ + --arg rknn_commit 42aa1d426c0a9e0869b6374edba009f7208a1926 \ + --arg inputs_lock_sha256 "$(sha256sum "$PROJECT_DIR/inputs.lock.json" | awk '{print $1}')" \ + --arg debian_lock_sha256 "$(sha256sum "$PROJECT_DIR/packages/debian-packages.lock.tsv" | awk '{print $1}')" \ + --arg raw_sha256 "$raw_sha" \ + --arg compressed_sha256 "$compressed_sha" \ + --argjson image_size_mib "$IMAGE_SIZE_MIB" \ + --argjson dependency_package_count "$expected_deb_count" \ + --argjson ssh_key_fingerprints "$fingerprints_json" \ + '{ + schema_version: 1, + build_id: $build_id, + board: "turing-rk1", + release: "debian-trixie-13.6", + kernel: "6.1.115-vendor-rk35xx", + gpu_stack: "panthor+mesa-panfrost+panvk", + base: {file: $base_file, sha256: $base_sha256}, + media_package: {file: $media_deb, sha256: $media_deb_sha256}, + rknn: {version: $rknn_version, commit: $rknn_commit}, + locks: {inputs_sha256: $inputs_lock_sha256, debian_packages_sha256: $debian_lock_sha256}, + artifacts: { + raw_sha256: $raw_sha256, + compressed_sha256: $compressed_sha256, + image_size_mib: $image_size_mib + }, + dependency_package_count: $dependency_package_count, + ssh_key_fingerprints: $ssh_key_fingerprints + }' >"${OUTPUT_DIR}/${OUTPUT_BASENAME}.manifest.json" + +echo "Built ${OUTPUT_DIR}/${OUTPUT_BASENAME}.img.xz" diff --git a/image/inject-ext4-tree.sh b/image/inject-ext4-tree.sh new file mode 100755 index 0000000..f5a73d5 --- /dev/null +++ b/image/inject-ext4-tree.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +[[ $# -eq 2 ]] || { echo "Usage: $0 ROOTFS.ext4 TREE" >&2; exit 2; } +ROOTFS=$1 +TREE=$2 +[[ -f "$ROOTFS" && -d "$TREE" ]] || { echo "Invalid rootfs or tree" >&2; exit 2; } + +COMMANDS=$(mktemp) +trap 'rm -f -- "$COMMANDS"' EXIT + +escape_debugfs() { + local value=$1 + value=${value//\\/\\\\} + value=${value//\"/\\\"} + printf '%s' "$value" +} + +while IFS= read -r -d '' source_path; do + relative_path=${source_path#"$TREE"/} + target_path="/${relative_path}" + quoted_target=$(escape_debugfs "$target_path") + if [[ -d "$source_path" && ! -L "$source_path" ]]; then + printf 'mkdir "%s"\n' "$quoted_target" >>"$COMMANDS" + fi +done < <(find "$TREE" -mindepth 1 -type d -print0 | sort -z) + +while IFS= read -r -d '' source_path; do + relative_path=${source_path#"$TREE"/} + target_path="/${relative_path}" + quoted_source=$(escape_debugfs "$source_path") + quoted_target=$(escape_debugfs "$target_path") + if [[ -L "$source_path" ]]; then + printf 'rm "%s"\n' "$quoted_target" >>"$COMMANDS" + printf 'symlink "%s" "%s"\n' "$quoted_target" "$(escape_debugfs "$(readlink "$source_path")")" >>"$COMMANDS" + elif [[ -f "$source_path" ]]; then + printf 'rm "%s"\n' "$quoted_target" >>"$COMMANDS" + printf 'write "%s" "%s"\n' "$quoted_source" "$quoted_target" >>"$COMMANDS" + fi +done < <(find "$TREE" -mindepth 1 \( -type f -o -type l \) -print0 | sort -z) + +while IFS= read -r -d '' source_path; do + relative_path=${source_path#"$TREE"/} + target_path="/${relative_path}" + quoted_target=$(escape_debugfs "$target_path") + permissions=$(stat -c '%a' "$source_path") + if [[ -d "$source_path" && ! -L "$source_path" ]]; then + file_type=040000 + elif [[ -L "$source_path" ]]; then + file_type=0120000 + else + file_type=0100000 + fi + printf 'set_inode_field "%s" uid 0\n' "$quoted_target" >>"$COMMANDS" + printf 'set_inode_field "%s" gid 0\n' "$quoted_target" >>"$COMMANDS" + printf 'set_inode_field "%s" mode 0%o\n' "$quoted_target" "$((file_type | 8#$permissions))" >>"$COMMANDS" +done < <(find "$TREE" -mindepth 1 -print0 | sort -z) + +# debugfs reports benign "already exists" and "file not found" diagnostics for +# idempotent mkdir/rm operations. The caller performs explicit postconditions. +debugfs -w -f "$COMMANDS" "$ROOTFS" >/dev/null 2>&1 diff --git a/image/overlay/etc/apt/apt.conf.d/20auto-upgrades b/image/overlay/etc/apt/apt.conf.d/20auto-upgrades new file mode 100644 index 0000000..8d6d7c8 --- /dev/null +++ b/image/overlay/etc/apt/apt.conf.d/20auto-upgrades @@ -0,0 +1,2 @@ +APT::Periodic::Update-Package-Lists "1"; +APT::Periodic::Unattended-Upgrade "1"; diff --git a/image/overlay/etc/apt/apt.conf.d/52unattended-upgrades-rk1-media b/image/overlay/etc/apt/apt.conf.d/52unattended-upgrades-rk1-media new file mode 100644 index 0000000..f52c077 --- /dev/null +++ b/image/overlay/etc/apt/apt.conf.d/52unattended-upgrades-rk1-media @@ -0,0 +1,17 @@ +#clear Unattended-Upgrade::Allowed-Origins; +#clear Unattended-Upgrade::Origins-Pattern; +Unattended-Upgrade::Origins-Pattern { + "origin=Debian,codename=${distro_codename}-security,label=Debian-Security"; +}; +Unattended-Upgrade::Package-Blacklist { + "^linux-image-.*$"; + "^linux-dtb-.*$"; + "^linux-u-boot-.*$"; + "^armbian-bsp-.*$"; + "^mesa-.*$"; + "^kodi.*$"; + "^rknn.*$"; + "^rockchip.*$"; +}; +Unattended-Upgrade::Automatic-Reboot "false"; +Unattended-Upgrade::Remove-Unused-Kernel-Packages "false"; diff --git a/image/overlay/etc/hostname b/image/overlay/etc/hostname new file mode 100644 index 0000000..995bf6d --- /dev/null +++ b/image/overlay/etc/hostname @@ -0,0 +1 @@ +rk1-media diff --git a/image/overlay/etc/ssh/sshd_config.d/90-rk1-media.conf b/image/overlay/etc/ssh/sshd_config.d/90-rk1-media.conf new file mode 100644 index 0000000..850cbee --- /dev/null +++ b/image/overlay/etc/ssh/sshd_config.d/90-rk1-media.conf @@ -0,0 +1,7 @@ +PermitRootLogin no +PasswordAuthentication no +KbdInteractiveAuthentication no +PubkeyAuthentication yes +AuthenticationMethods publickey +PermitEmptyPasswords no +AllowUsers rkadmin diff --git a/image/overlay/etc/systemd/system/multi-user.target.wants/rk1-media-provision.service b/image/overlay/etc/systemd/system/multi-user.target.wants/rk1-media-provision.service new file mode 100644 index 0000000..1fe5ef6 --- /dev/null +++ b/image/overlay/etc/systemd/system/multi-user.target.wants/rk1-media-provision.service @@ -0,0 +1 @@ +../rk1-media-provision.service diff --git a/image/overlay/etc/systemd/system/rk1-media-identity.service b/image/overlay/etc/systemd/system/rk1-media-identity.service new file mode 100644 index 0000000..5326dd7 --- /dev/null +++ b/image/overlay/etc/systemd/system/rk1-media-identity.service @@ -0,0 +1,15 @@ +[Unit] +Description=RK1 media appliance identity and SSH hardening +DefaultDependencies=no +After=local-fs.target systemd-remount-fs.service +Before=ssh.service sshd.service getty.target serial-getty.target multi-user.target +ConditionPathExists=!/var/lib/rk1-media/identity-ready + +[Service] +Type=oneshot +RuntimeDirectory=sshd +RuntimeDirectoryMode=0755 +ExecStart=/usr/local/sbin/rk1-media-identity + +[Install] +WantedBy=sysinit.target diff --git a/image/overlay/etc/systemd/system/rk1-media-provision.service b/image/overlay/etc/systemd/system/rk1-media-provision.service new file mode 100644 index 0000000..25e22ad --- /dev/null +++ b/image/overlay/etc/systemd/system/rk1-media-provision.service @@ -0,0 +1,14 @@ +[Unit] +Description=Install the offline RK1 media appliance payload +After=local-fs.target systemd-udev-settle.service rk1-media-identity.service +Before=apt-daily.service apt-daily-upgrade.service kodi-rk.service +ConditionPathExists=!/var/lib/rk1-media/provisioned + +[Service] +Type=oneshot +ExecStart=/usr/local/sbin/rk1-media-provision +RemainAfterExit=yes +TimeoutStartSec=20min + +[Install] +WantedBy=multi-user.target diff --git a/image/overlay/etc/systemd/system/ssh.service.d/10-rk1-media-identity.conf b/image/overlay/etc/systemd/system/ssh.service.d/10-rk1-media-identity.conf new file mode 100644 index 0000000..8c6db5c --- /dev/null +++ b/image/overlay/etc/systemd/system/ssh.service.d/10-rk1-media-identity.conf @@ -0,0 +1,3 @@ +[Unit] +Requires=rk1-media-identity.service +After=rk1-media-identity.service diff --git a/image/overlay/etc/systemd/system/sysinit.target.wants/rk1-media-identity.service b/image/overlay/etc/systemd/system/sysinit.target.wants/rk1-media-identity.service new file mode 100644 index 0000000..b91973d --- /dev/null +++ b/image/overlay/etc/systemd/system/sysinit.target.wants/rk1-media-identity.service @@ -0,0 +1 @@ +../../rk1-media-identity.service diff --git a/image/overlay/usr/local/sbin/rk1-media-identity b/image/overlay/usr/local/sbin/rk1-media-identity new file mode 100755 index 0000000..6b3ca41 --- /dev/null +++ b/image/overlay/usr/local/sbin/rk1-media-identity @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +install -d -m 0755 /var/lib/rk1-media +hostnamectl set-hostname rk1-media 2>/dev/null || printf 'rk1-media\n' >/etc/hostname + +for group_name in audio video render input; do + getent group "$group_name" >/dev/null || groupadd --system "$group_name" +done + +getent group rkadmin >/dev/null || groupadd rkadmin +if ! id rkadmin >/dev/null 2>&1; then + useradd --create-home --gid rkadmin --shell /bin/bash --groups sudo,adm,systemd-journal,video,render,audio,input rkadmin +fi +usermod --password '!' rkadmin +install -d -o rkadmin -g rkadmin -m 0700 /home/rkadmin/.ssh +install -o rkadmin -g rkadmin -m 0600 /opt/rk1-seed/authorized_keys /home/rkadmin/.ssh/authorized_keys + +install -d -m 0755 /etc/sudoers.d +printf 'rkadmin ALL=(ALL:ALL) NOPASSWD: ALL\n' >/etc/sudoers.d/90-rkadmin +chmod 0440 /etc/sudoers.d/90-rkadmin +visudo --check --file=/etc/sudoers.d/90-rkadmin >/dev/null + +usermod --password '!' root +if grep -q '^127\.0\.1\.1[[:space:]]' /etc/hosts; then + sed -i 's/^127\.0\.1\.1[[:space:]].*/127.0.1.1\trk1-media/' /etc/hosts +else + printf '127.0.1.1\trk1-media\n' >>/etc/hosts +fi +rm -f /root/.not_logged_in_yet /etc/ssh/ssh_host_* +ssh-keygen -A +# ssh.service normally creates this through RuntimeDirectory=sshd. This +# validation runs before ssh.service, so it must provide the directory itself. +install -d -o root -g root -m 0755 /run/sshd +/usr/sbin/sshd -t +touch /var/lib/rk1-media/identity-ready diff --git a/image/overlay/usr/local/sbin/rk1-media-provision b/image/overlay/usr/local/sbin/rk1-media-provision new file mode 100755 index 0000000..c4e31d6 --- /dev/null +++ b/image/overlay/usr/local/sbin/rk1-media-provision @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +exec > >(tee -a /var/log/rk1-media-provision.log) 2>&1 +export DEBIAN_FRONTEND=noninteractive + +install -d -m 0755 /var/lib/rk1-media +seed_archives=/opt/rk1-seed/debs +# APT's --no-download mode only accepts dependency archives from its configured +# cache, even when every package is also named as an absolute local path. Make +# the immutable image seed that cache rather than copying it into /var/cache. +install -d -o root -g root -m 0755 "$seed_archives" +install -d -o _apt -g root -m 0700 "$seed_archives/partial" +mapfile -d '' debs < <(find "$seed_archives" -maxdepth 1 -type f -name '*.deb' -print0 | sort -z) +((${#debs[@]} > 0)) || { echo 'Offline package bundle is empty' >&2; exit 1; } + +apt-get \ + -o Dir::Cache::archives="$seed_archives" \ + -o APT::Keep-Downloaded-Packages=true \ + --no-download --no-install-recommends --yes install "${debs[@]}" +ldconfig +udevadm control --reload-rules +# MODE/GROUP assignments for device nodes are applied on add events. The misc +# trigger covers the vendor MPP, RGA, and RKNN control nodes. +udevadm trigger --action=add --subsystem-match=drm || true +udevadm trigger --action=add --subsystem-match=dma_heap || true +udevadm trigger --action=add --subsystem-match=misc || true +udevadm settle + +install -d -m 0755 /usr/share/rk1-media +held_packages=() +while IFS= read -r package_name; do + case "$package_name" in + linux-image-*|linux-dtb-*|linux-u-boot-*|armbian-bsp-*) + apt-mark hold "$package_name" >/dev/null + held_packages+=("$package_name") + ;; + esac +done < <(dpkg-query --show --showformat='${binary:Package}\n') +printf '%s\n' "${held_packages[@]}" | LC_ALL=C sort -u \ + >/usr/share/rk1-media/held-packages.txt +dpkg-query --show --showformat='${binary:Package}\t${Version}\t${Architecture}\n' \ + | LC_ALL=C sort >/usr/share/rk1-media/package-manifest.tsv + +systemctl disable --now kodi-pulse.service pulseaudio.service 2>/dev/null || true +systemctl daemon-reload +systemctl enable avahi-daemon.service kodi-rk.service +# kodi-rk is ordered after this oneshot. Queue it without waiting so systemd +# can start it as soon as provisioning exits instead of deadlocking here. +systemctl --no-block start avahi-daemon.service kodi-rk.service +touch /var/lib/rk1-media/provisioned diff --git a/inputs.lock.json b/inputs.lock.json new file mode 100644 index 0000000..bb9a18b --- /dev/null +++ b/inputs.lock.json @@ -0,0 +1,24 @@ +{ + "schema_version": 1, + "locked_at": "2026-08-16T20:32:07Z", + "base_image": { + "description": "Armbian Community Debian 13 minimal image for Turing RK1", + "url": "https://github.com/armbian/community/releases/download/26.11.0-trunk.1/Armbian_community_26.11.0-trunk.1_Turing-rk1_trixie_vendor_6.1.115_minimal.img.xz", + "output_name": "Armbian_community_26.11.0-trunk.1_Turing-rk1_trixie_vendor_6.1.115_minimal.img.xz", + "size": 322369692, + "sha256": "cbbd16d90786331fbfbbb695614a53f5e87ea441ea37b425d986334318190cf6" + }, + "kodi_upstream_deb": { + "description": "armsurvivors Kodi GBM/RKMPP Debian Trixie release asset", + "url": "https://github.com/armsurvivors/kodi-rockchip-deb/releases/download/20260721-1741/kodi-rockchip-gbm_arm64_kodi_master_ffmpeg_81_trixie.deb", + "output_name": "kodi-rockchip-gbm_20260721-1741_trixie_arm64.deb", + "size": 147977956, + "sha256": "f9eed4c02d80f12b443604026fb00c4a6ff5ddd1fe5e6c554d81852a849d261a" + }, + "rknn_toolkit2": { + "repository": "https://github.com/airockchip/rknn-toolkit2.git", + "tag": "v2.3.2", + "commit": "42aa1d426c0a9e0869b6374edba009f7208a1926", + "bundle_directory": "rknn-toolkit2-v2.3.2-minimal" + } +} diff --git a/keys/rk1-media.pub b/keys/rk1-media.pub new file mode 100644 index 0000000..8caded8 --- /dev/null +++ b/keys/rk1-media.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG9jdtdu7zOd98m+tTreOwXk5b5QB5wA4zZV3f/mIb/v rk1-media diff --git a/media/.gitignore b/media/.gitignore new file mode 100644 index 0000000..faf0ec3 --- /dev/null +++ b/media/.gitignore @@ -0,0 +1,2 @@ +/out/ + diff --git a/media/README.md b/media/README.md new file mode 100644 index 0000000..258236c --- /dev/null +++ b/media/README.md @@ -0,0 +1,108 @@ +# RK1 media stack + +This directory builds an ARM64-only Kodi/GBM appliance stack for an RK3588 +running Armbian Debian 13 (Trixie) with the Rockchip vendor 6.1 kernel. All +third-party inputs are immutable Git commits recorded in `sources.lock.json`. +The result is a Debian package whose custom libraries and programs live under +`/opt/rkmedia`; it does not replace Debian's FFmpeg or Kodi packages. + +## Why the compatibility MPP and RGA branches are pinned + +The current `kodi-rockchip-deb` recipe uses nyanmisaka's `jellyfin-mpp` and +`jellyfin-rga` branches. FFmpeg-Rockchip 8.1 checks for MPP package version +1.3.9 and the `mpp_buffer_sync_begin_f` symbol, and checks the RGA APIs +`c_RkRgaBlit` and `querystring`. These exact branches satisfy that interface. +The official Rockchip MPP/RGA heads inspected when this lock was generated are +recorded as reference metadata, but are deliberately not substituted into the +matched build without a complete hardware regression run. + +## Build + +Run this on an aarch64 Debian Trixie installation. The native build is large and +can take several hours. + +```sh +sudo ./scripts/install-build-deps.sh --install +./scripts/build-media.sh +``` + +Outputs are written to `out/` by default: + +- `rk1-media-stack__arm64.deb` +- an unpacked `stage/` tree for inspection +- cloned, detached source trees and component build directories + +Useful options: + +```sh +./scripts/fetch-sources.sh --dest /path/to/sources +./scripts/build-media.sh --source-dir /path/to/sources --work-dir /path/to/work --output-dir /path/to/output --jobs 8 +./scripts/build-media.sh --no-fetch +``` + +Set `RKMEDIA_ALLOW_UNSUPPORTED_HOST=1` only for development. Packages built on +anything other than Trixie are unsupported because their generated shared +library dependencies will describe the build host, not the target image. + +## Fast candidate repack + +For image composition and hardware validation, the pinned July 2026 upstream +Trixie package can be deterministically repacked in a few minutes instead of +performing the multi-hour native compile: + +```sh +./scripts/repack-upstream-deb.sh +``` + +The script downloads the exact asset in `upstream-deb.lock.json`, verifies its +size and SHA-256 before extraction, removes approximately 270 MB of static +FFmpeg archives and all development pkg-config files, and emits the same +`rk1-media-stack` package format. Kodi remains at `/usr/local`, its compiled +prefix. FFmpeg, FFprobe, MPP, RGA, dav1d, and display-info move under +`/opt/rkmedia`; the launcher supplies the corresponding library path. + +Use `--deb PATH` to repack an already-downloaded copy without network access. +The prebuilt asset is reproducible as an input but its original recipe used +moving source branches; its verified asset digest, rather than reconstructed +Git commit guesses, is the provenance boundary. + +## Install and operate + +Install with APT so runtime dependencies are resolved: + +```sh +sudo apt install ./rk1-media-stack__arm64.deb +sudo systemctl enable --now kodi-rk.service +``` + +The package creates a locked `kodi` system account, grants it only the existing +`video`, `render`, `audio`, and `input` group memberships, and gives it persistent +state under `/var/lib/kodi`. Kodi takes DRM master on tty1, so a display manager +or another program holding the KMS device must not be active. + +The service is enabled during package configuration but is not started inside +an image-build chroot. Edit `/etc/rkmedia/kodi.env` for supported environment +overrides, then restart `kodi-rk.service`. + +The following commands are exposed without replacing system FFmpeg: + +```sh +ffmpeg-rk -hide_banner -decoders +ffmpeg-rk -hide_banner -encoders +ffmpeg-rk -hide_banner -filters +ffprobe-rk media-file.mkv +``` + +Expected hardware entries include `h264_rkmpp`, `hevc_rkmpp`, `vp9_rkmpp`, +`av1_rkmpp`, `mjpeg_rkmpp`, and the `scale_rkrga`, `vpp_rkrga`, and +`overlay_rkrga` filters. Runtime access is provided through conservative udev +rules for the DRM, dma-heap, RGA, IEP, VPU, and MPP device nodes. + +## Boundaries + +- The package does not install or select the kernel, device tree, Panthor + overlay, Mesa, firmware, or bootloader. Those belong to the parent image. +- The package does not claim HDMI link, HDR, passthrough, or codec acceptance; + those require testing on the actual RK1/carrier/TV combination. +- Kodi is pinned to a mainline commit because the current Rockchip GBM work is + newer than a stable Kodi release. Update the lock only as a tested set. diff --git a/media/VERSION b/media/VERSION new file mode 100644 index 0000000..92af7e0 --- /dev/null +++ b/media/VERSION @@ -0,0 +1,2 @@ +20260816.1 + diff --git a/media/packaging/control.in b/media/packaging/control.in new file mode 100644 index 0000000..f64d4e3 --- /dev/null +++ b/media/packaging/control.in @@ -0,0 +1,15 @@ +Package: rk1-media-stack +Version: @VERSION@ +Section: video +Priority: optional +Architecture: arm64 +Maintainer: RK1 Media Image Builder +Installed-Size: @INSTALLED_SIZE@ +Depends: @DEPENDS@ +Recommends: armbian-firmware, libgl1-mesa-dri, mesa-vulkan-drivers +Homepage: https://github.com/armsurvivors/kodi-rockchip-deb +X-RKMedia-Provenance-SHA256: @LOCK_SHA256@ +Description: isolated RK3588 Kodi GBM and FFmpeg hardware media stack + Kodi for direct GBM/GLES rendering plus a matched FFmpeg-Rockchip, MPP, + RGA, dav1d, and libdisplay-info runtime. Custom software is isolated under + /opt/rkmedia and does not replace the distribution FFmpeg packages. diff --git a/media/packaging/postinst b/media/packaging/postinst new file mode 100755 index 0000000..8634a06 --- /dev/null +++ b/media/packaging/postinst @@ -0,0 +1,34 @@ +#!/bin/sh + +set -e + +if ! getent group kodi >/dev/null 2>&1; then + addgroup --system kodi +fi +if ! getent passwd kodi >/dev/null 2>&1; then + adduser --system --ingroup kodi --home /var/lib/kodi --no-create-home \ + --disabled-login --shell /usr/sbin/nologin kodi +fi + +for group in video render audio input; do + if getent group "${group}" >/dev/null 2>&1; then + adduser kodi "${group}" >/dev/null + fi +done + +systemd-tmpfiles --create rkmedia.conf >/dev/null 2>&1 || true +udevadm control --reload-rules >/dev/null 2>&1 || true +udevadm trigger --action=change --subsystem-match=drm >/dev/null 2>&1 || true +udevadm trigger --action=change --subsystem-match=dma_heap >/dev/null 2>&1 || true +udevadm trigger --action=change --subsystem-match=misc >/dev/null 2>&1 || true +udevadm settle >/dev/null 2>&1 || true + +if command -v deb-systemd-helper >/dev/null 2>&1; then + deb-systemd-helper unmask kodi-rk.service >/dev/null || true + deb-systemd-helper enable kodi-rk.service >/dev/null || true +fi +if [ -d /run/systemd/system ]; then + systemctl daemon-reload >/dev/null || true +fi + +exit 0 diff --git a/media/packaging/postrm b/media/packaging/postrm new file mode 100755 index 0000000..49071b6 --- /dev/null +++ b/media/packaging/postrm @@ -0,0 +1,16 @@ +#!/bin/sh + +set -e + +if command -v deb-systemd-helper >/dev/null 2>&1; then + if [ "$1" = purge ]; then + deb-systemd-helper purge kodi-rk.service >/dev/null || true + fi +fi +if [ -d /run/systemd/system ]; then + systemctl daemon-reload >/dev/null || true +fi + +# Deliberately retain /var/lib/kodi and the locked account on removal. The +# media database and settings must never be deleted by a package uninstall. +exit 0 diff --git a/media/packaging/prerm b/media/packaging/prerm new file mode 100755 index 0000000..1ce89d7 --- /dev/null +++ b/media/packaging/prerm @@ -0,0 +1,8 @@ +#!/bin/sh + +set -e + +if [ "$1" = remove ] && [ -d /run/systemd/system ]; then + systemctl --no-reload stop kodi-rk.service >/dev/null 2>&1 || true +fi +exit 0 diff --git a/media/packaging/rootfs/etc/rkmedia/kodi.env b/media/packaging/rootfs/etc/rkmedia/kodi.env new file mode 100644 index 0000000..93dd2c6 --- /dev/null +++ b/media/packaging/rootfs/etc/rkmedia/kodi.env @@ -0,0 +1,8 @@ +# Environment loaded by kodi-rk.service. This file is preserved across package +# upgrades. Kodi command-line arguments live in the unit so values here never +# undergo unsafe shell word splitting. +KODI_DATA=/var/lib/kodi/.kodi +CRASHLOG_DIR=/var/lib/kodi +XDG_CACHE_HOME=/var/cache/kodi +MESA_SHADER_CACHE_DIR=/var/cache/kodi/mesa_shader_cache + diff --git a/media/packaging/rootfs/opt/rkmedia/bin/ffmpeg-rk b/media/packaging/rootfs/opt/rkmedia/bin/ffmpeg-rk new file mode 100755 index 0000000..e998168 --- /dev/null +++ b/media/packaging/rootfs/opt/rkmedia/bin/ffmpeg-rk @@ -0,0 +1,7 @@ +#!/bin/sh + +set -eu +prefix=/opt/rkmedia +export LD_LIBRARY_PATH="${prefix}/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" +exec "${prefix}/bin/ffmpeg" "$@" + diff --git a/media/packaging/rootfs/opt/rkmedia/bin/ffprobe-rk b/media/packaging/rootfs/opt/rkmedia/bin/ffprobe-rk new file mode 100755 index 0000000..7b67e38 --- /dev/null +++ b/media/packaging/rootfs/opt/rkmedia/bin/ffprobe-rk @@ -0,0 +1,7 @@ +#!/bin/sh + +set -eu +prefix=/opt/rkmedia +export LD_LIBRARY_PATH="${prefix}/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" +exec "${prefix}/bin/ffprobe" "$@" + diff --git a/media/packaging/rootfs/opt/rkmedia/bin/kodi-rk b/media/packaging/rootfs/opt/rkmedia/bin/kodi-rk new file mode 100755 index 0000000..d0f705a --- /dev/null +++ b/media/packaging/rootfs/opt/rkmedia/bin/kodi-rk @@ -0,0 +1,25 @@ +#!/bin/sh + +set -eu +prefix=/opt/rkmedia +data_dir="${KODI_DATA:-${HOME}/.kodi}" +userdata_dir="${data_dir}/userdata" + +export PATH="${prefix}/bin:${PATH}" +export LD_LIBRARY_PATH="${prefix}/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" + +mkdir -p "${userdata_dir}" +if [ ! -e "${userdata_dir}/guisettings.xml" ]; then + cp /usr/share/rkmedia/kodi/guisettings.xml "${userdata_dir}/guisettings.xml" +fi + +if [ -x "${prefix}/bin/kodi" ]; then + kodi_launcher="${prefix}/bin/kodi" +elif [ -x /usr/local/bin/kodi ]; then + kodi_launcher=/usr/local/bin/kodi +else + printf 'kodi-rk: no Kodi launcher found under /opt/rkmedia or /usr/local\n' >&2 + exit 127 +fi + +exec "${kodi_launcher}" "$@" diff --git a/media/packaging/rootfs/usr/lib/systemd/system/kodi-rk.service b/media/packaging/rootfs/usr/lib/systemd/system/kodi-rk.service new file mode 100644 index 0000000..be36974 --- /dev/null +++ b/media/packaging/rootfs/usr/lib/systemd/system/kodi-rk.service @@ -0,0 +1,51 @@ +[Unit] +Description=Kodi RK3588 GBM appliance +Documentation=https://github.com/armsurvivors/kodi-rockchip-deb +Wants=network-online.target +After=systemd-user-sessions.service network-online.target sound.target +Conflicts=display-manager.service getty@tty1.service + +[Service] +Type=simple +User=kodi +Group=kodi +SupplementaryGroups=video render audio input +Environment=HOME=/var/lib/kodi +Environment=XDG_RUNTIME_DIR=/run/kodi +Environment=LD_LIBRARY_PATH=/opt/rkmedia/lib +EnvironmentFile=-/etc/rkmedia/kodi.env +RuntimeDirectory=kodi +RuntimeDirectoryMode=0700 +StateDirectory=kodi +StateDirectoryMode=0750 +CacheDirectory=kodi +CacheDirectoryMode=0750 +PAMName=login +UtmpIdentifier=tty1 +UtmpMode=user +StandardInput=tty +TTYPath=/dev/tty1 +TTYReset=yes +TTYVHangup=yes +TTYVTDisallocate=yes +ExecStart=/opt/rkmedia/bin/kodi-rk --standalone --logging=console --windowing=gbm --audio-backend=alsa +Restart=always +RestartSec=2s +TimeoutStopSec=15s +LimitNOFILE=16384 +NoNewPrivileges=yes +PrivateTmp=yes +ProtectSystem=full +ProtectHome=yes +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectControlGroups=yes +RestrictNamespaces=yes +RestrictSUIDSGID=yes +LockPersonality=yes +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK +CapabilityBoundingSet=CAP_SYS_TTY_CONFIG +AmbientCapabilities=CAP_SYS_TTY_CONFIG + +[Install] +WantedBy=multi-user.target diff --git a/media/packaging/rootfs/usr/lib/tmpfiles.d/rkmedia.conf b/media/packaging/rootfs/usr/lib/tmpfiles.d/rkmedia.conf new file mode 100644 index 0000000..22372ad --- /dev/null +++ b/media/packaging/rootfs/usr/lib/tmpfiles.d/rkmedia.conf @@ -0,0 +1,4 @@ +d /var/lib/kodi 0750 kodi kodi - +d /var/lib/kodi/.kodi 0750 kodi kodi - +d /var/cache/kodi 0750 kodi kodi - + diff --git a/media/packaging/rootfs/usr/lib/udev/rules.d/70-rkmedia.rules b/media/packaging/rootfs/usr/lib/udev/rules.d/70-rkmedia.rules new file mode 100644 index 0000000..ee3698e --- /dev/null +++ b/media/packaging/rootfs/usr/lib/udev/rules.d/70-rkmedia.rules @@ -0,0 +1,22 @@ +# Primary and render DRM nodes used by Kodi GBM and DRM PRIME. +SUBSYSTEM=="drm", KERNEL=="card[0-9]*", GROUP="video", MODE="0660", TAG+="uaccess" +SUBSYSTEM=="drm", KERNEL=="renderD[0-9]*", GROUP="render", MODE="0660", TAG+="uaccess" + +# Rockchip vendor-kernel media accelerators. Optional compatibility names are +# included because their exact node names vary across BSP kernel revisions. +SUBSYSTEM=="dma_heap", GROUP="video", MODE="0660" +KERNEL=="rga", GROUP="video", MODE="0660" +KERNEL=="iep", GROUP="video", MODE="0660" +KERNEL=="mpp_service", GROUP="video", MODE="0660" +KERNEL=="mpp-service", GROUP="video", MODE="0660" +KERNEL=="vpu_service", GROUP="video", MODE="0660" +KERNEL=="vpu-service", GROUP="video", MODE="0660" +KERNEL=="hevc_service", GROUP="video", MODE="0660" +KERNEL=="hevc-service", GROUP="video", MODE="0660" +KERNEL=="rkvdec", GROUP="video", MODE="0660" +KERNEL=="rkvenc", GROUP="video", MODE="0660" +KERNEL=="vepu", GROUP="video", MODE="0660" +KERNEL=="h265e", GROUP="video", MODE="0660" + +# Rockchip NPU control/render compatibility nodes used by RKNN diagnostics. +KERNEL=="rknpu*", GROUP="render", MODE="0660" diff --git a/media/packaging/rootfs/usr/share/rkmedia/kodi/guisettings.xml b/media/packaging/rootfs/usr/share/rkmedia/kodi/guisettings.xml new file mode 100644 index 0000000..3f50ce3 --- /dev/null +++ b/media/packaging/rootfs/usr/share/rkmedia/kodi/guisettings.xml @@ -0,0 +1,5 @@ + + true + true + 0 + diff --git a/media/scripts/build-media.sh b/media/scripts/build-media.sh new file mode 100755 index 0000000..a3ee799 --- /dev/null +++ b/media/scripts/build-media.sh @@ -0,0 +1,226 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +# shellcheck source=lib.sh +. "${SCRIPT_DIR}/lib.sh" + +LOCK_FILE="${RKMEDIA_ROOT}/sources.lock.json" +WORK_DIR="${RKMEDIA_ROOT}/out/work" +SOURCE_DIR="${RKMEDIA_ROOT}/out/sources" +OUTPUT_DIR="${RKMEDIA_ROOT}/out" +JOBS="$(nproc)" +FETCH=1 +PACKAGE_VERSION="$(tr -d '[:space:]' < "${RKMEDIA_ROOT}/VERSION")" +RUNTIME_PREFIX=/opt/rkmedia + +usage() { + cat <<'EOF' +Usage: build-media.sh [options] + + --source-dir DIR Locked source checkouts (default: out/sources) + --work-dir DIR Build work directory (default: out/work) + --output-dir DIR Package output directory (default: out) + --jobs N Parallel jobs (default: nproc) + --no-fetch Require sources to exist; never access the network + --version VERSION Debian package version (default: VERSION file) +EOF +} + +while (($#)); do + case "$1" in + --source-dir) [[ $# -ge 2 ]] || die "--source-dir requires a directory"; SOURCE_DIR="$2"; shift 2 ;; + --work-dir) [[ $# -ge 2 ]] || die "--work-dir requires a directory"; WORK_DIR="$2"; shift 2 ;; + --output-dir) [[ $# -ge 2 ]] || die "--output-dir requires a directory"; OUTPUT_DIR="$2"; shift 2 ;; + --jobs) [[ $# -ge 2 ]] || die "--jobs requires a number"; JOBS="$2"; shift 2 ;; + --no-fetch) FETCH=0; shift ;; + --version) [[ $# -ge 2 ]] || die "--version requires a value"; PACKAGE_VERSION="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown argument: $1" ;; + esac +done + +[[ "${JOBS}" =~ ^[1-9][0-9]*$ ]] || die "--jobs must be a positive integer" +[[ "${PACKAGE_VERSION}" =~ ^[0-9A-Za-z.+:~-]+$ ]] || die "invalid Debian package version: ${PACKAGE_VERSION}" +require_arm64_trixie +for command_name in cmake git jq meson ninja pkg-config make patch readelf strip; do + need_command "${command_name}" +done + +SOURCE_DIR="$(absolute_path "${SOURCE_DIR}")" +WORK_DIR="$(absolute_path "${WORK_DIR}")" +OUTPUT_DIR="$(absolute_path "${OUTPUT_DIR}")" +mkdir -p -- "${SOURCE_DIR}" "${WORK_DIR}" "${OUTPUT_DIR}" + +if ((FETCH)); then + "${SCRIPT_DIR}/fetch-sources.sh" --lock "${LOCK_FILE}" --dest "${SOURCE_DIR}" +fi + +while IFS=$'\t' read -r name commit; do + dir="${SOURCE_DIR}/${name}" + [[ -d "${dir}/.git" ]] || die "missing locked source: ${dir}" + [[ "$(git -C "${dir}" rev-parse HEAD)" == "${commit}" ]] || die "source commit mismatch: ${name}" +done < <(jq -r '.sources[] | select(.build == true) | [.name, .commit] | @tsv' "${LOCK_FILE}") + +BUILD_ROOT="${WORK_DIR}/build" +BUILD_PREFIX="${WORK_DIR}/prefix" +STAGE_DIR="${WORK_DIR}/stage" +reset_child_dir "${WORK_DIR}" "${BUILD_ROOT}" +reset_child_dir "${WORK_DIR}" "${BUILD_PREFIX}" +reset_child_dir "${WORK_DIR}" "${STAGE_DIR}" + +export SOURCE_DATE_EPOCH +SOURCE_DATE_EPOCH="$(jq -er '.source_date_epoch' "${LOCK_FILE}")" +export CFLAGS="${CFLAGS:--O2 -pipe -ffile-prefix-map=${WORK_DIR}=. -fdebug-prefix-map=${WORK_DIR}=.}" +export CXXFLAGS="${CXXFLAGS:--O2 -pipe -ffile-prefix-map=${WORK_DIR}=. -fdebug-prefix-map=${WORK_DIR}=.}" +export LDFLAGS="${LDFLAGS:-}" + +log "building MPP" +cmake -S "${SOURCE_DIR}/mpp" -B "${BUILD_ROOT}/mpp" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="${BUILD_PREFIX}" \ + -DCMAKE_INSTALL_LIBDIR=lib \ + -DBUILD_SHARED_LIBS=ON \ + -DBUILD_TEST=OFF +cmake --build "${BUILD_ROOT}/mpp" --parallel "${JOBS}" +cmake --install "${BUILD_ROOT}/mpp" + +log "building RGA" +meson setup "${BUILD_ROOT}/rga" "${SOURCE_DIR}/rga" \ + --prefix="${BUILD_PREFIX}" \ + --libdir=lib \ + --buildtype=release \ + --default-library=shared \ + -Dcpp_args=-fpermissive \ + -Dlibdrm=false \ + -Dlibrga_demo=false +meson compile -C "${BUILD_ROOT}/rga" -j "${JOBS}" +meson install -C "${BUILD_ROOT}/rga" + +log "building dav1d" +meson setup "${BUILD_ROOT}/dav1d" "${SOURCE_DIR}/dav1d" \ + --prefix="${BUILD_PREFIX}" \ + --libdir=lib \ + --buildtype=release \ + --default-library=shared \ + -Denable_tools=false \ + -Denable_tests=false \ + -Denable_examples=false \ + -Denable_docs=false +meson compile -C "${BUILD_ROOT}/dav1d" -j "${JOBS}" +meson install -C "${BUILD_ROOT}/dav1d" + +export PKG_CONFIG_PATH="${BUILD_PREFIX}/lib/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}" +export LD_LIBRARY_PATH="${BUILD_PREFIX}/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" + +log "building FFmpeg-Rockchip" +mkdir -p -- "${BUILD_ROOT}/ffmpeg" +pushd "${BUILD_ROOT}/ffmpeg" >/dev/null +"${SOURCE_DIR}/ffmpeg-rockchip/configure" \ + --prefix="${BUILD_PREFIX}" \ + --libdir="${BUILD_PREFIX}/lib" \ + --enable-shared \ + --disable-static \ + --disable-debug \ + --disable-doc \ + --enable-pic \ + --enable-gpl \ + --enable-version3 \ + --enable-libdrm \ + --enable-rkmpp \ + --enable-rkrga \ + --enable-libdav1d \ + --extra-cflags="-I${BUILD_PREFIX}/include" \ + --extra-ldflags="-L${BUILD_PREFIX}/lib -Wl,-rpath,${RUNTIME_PREFIX}/lib" +make -j "${JOBS}" +make install +popd >/dev/null + +FFMPEG_BUILD="${BUILD_PREFIX}/bin/ffmpeg" +for decoder in h264_rkmpp hevc_rkmpp vp9_rkmpp av1_rkmpp mjpeg_rkmpp; do + "${FFMPEG_BUILD}" -hide_banner -decoders 2>/dev/null | awk '{print $2}' | grep -Fxq "${decoder}" \ + || die "FFmpeg is missing decoder ${decoder}" +done +for encoder in h264_rkmpp hevc_rkmpp mjpeg_rkmpp; do + "${FFMPEG_BUILD}" -hide_banner -encoders 2>/dev/null | awk '{print $2}' | grep -Fxq "${encoder}" \ + || die "FFmpeg is missing encoder ${encoder}" +done +for filter in scale_rkrga vpp_rkrga overlay_rkrga; do + "${FFMPEG_BUILD}" -hide_banner -filters 2>/dev/null | awk '{print $2}' | grep -Fxq "${filter}" \ + || die "FFmpeg is missing filter ${filter}" +done + +log "building libdisplay-info" +meson setup "${BUILD_ROOT}/libdisplay-info" "${SOURCE_DIR}/libdisplay-info" \ + --prefix="${BUILD_PREFIX}" \ + --libdir=lib \ + --buildtype=release \ + --default-library=shared +meson compile -C "${BUILD_ROOT}/libdisplay-info" -j "${JOBS}" +meson install -C "${BUILD_ROOT}/libdisplay-info" + +log "building Kodi for GBM/GLES with external FFmpeg-Rockchip" +cmake -S "${SOURCE_DIR}/kodi" -B "${BUILD_ROOT}/kodi" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="${RUNTIME_PREFIX}" \ + -DCMAKE_INSTALL_RPATH="${RUNTIME_PREFIX}/lib" \ + -DCMAKE_BUILD_RPATH="${BUILD_PREFIX}/lib" \ + -DCMAKE_PREFIX_PATH="${BUILD_PREFIX}" \ + -DCORE_PLATFORM_NAME=gbm \ + -DAPP_RENDER_SYSTEM=gles \ + -DENABLE_INTERNAL_FFMPEG=OFF \ + -DFFMPEG_PATH="${BUILD_PREFIX}" \ + -DENABLE_INTERNAL_FMT=ON \ + -DENABLE_INTERNAL_FLATBUFFERS=ON +cmake --build "${BUILD_ROOT}/kodi" --parallel "${JOBS}" + +mkdir -p -- "${STAGE_DIR}${RUNTIME_PREFIX}" +cp -a -- "${BUILD_PREFIX}/." "${STAGE_DIR}${RUNTIME_PREFIX}/" +DESTDIR="${STAGE_DIR}" cmake --install "${BUILD_ROOT}/kodi" + +log "adding runtime integration and source provenance" +cp -a -- "${RKMEDIA_ROOT}/packaging/rootfs/." "${STAGE_DIR}/" +mkdir -p -- "${STAGE_DIR}/usr/bin" "${STAGE_DIR}/usr/share/doc/rk1-media-stack/licenses" +ln -sfn /opt/rkmedia/bin/ffmpeg-rk "${STAGE_DIR}/usr/bin/ffmpeg-rk" +ln -sfn /opt/rkmedia/bin/ffprobe-rk "${STAGE_DIR}/usr/bin/ffprobe-rk" +ln -sfn /opt/rkmedia/bin/kodi-rk "${STAGE_DIR}/usr/bin/kodi-rk" +install -m 0644 "${LOCK_FILE}" "${STAGE_DIR}/usr/share/doc/rk1-media-stack/sources.lock.json" + +while IFS=$'\t' read -r name license_path; do + source_license="${SOURCE_DIR}/${name}/${license_path}" + destination="${STAGE_DIR}/usr/share/doc/rk1-media-stack/licenses/${name}" + [[ -e "${source_license}" ]] || die "locked license path is missing: ${name}/${license_path}" + mkdir -p -- "${destination}" + cp -a -- "${source_license}" "${destination}/" +done < <(jq -r '.sources[] | select(.build == true) as $source | $source.license_files[] | [$source.name, .] | @tsv' "${LOCK_FILE}") + +rm -rf -- \ + "${STAGE_DIR}${RUNTIME_PREFIX}/include" \ + "${STAGE_DIR}${RUNTIME_PREFIX}/lib/pkgconfig" \ + "${STAGE_DIR}${RUNTIME_PREFIX}/share/pkgconfig" +find "${STAGE_DIR}${RUNTIME_PREFIX}" -type f \( -name '*.a' -o -name '*.la' \) -delete + +while IFS= read -r -d '' candidate; do + if file --brief "${candidate}" | grep -q '^ELF '; then + strip --strip-unneeded "${candidate}" || die "failed to strip ${candidate}" + fi +done < <(find "${STAGE_DIR}${RUNTIME_PREFIX}" -type f -print0) + +for required in \ + "${STAGE_DIR}${RUNTIME_PREFIX}/bin/ffmpeg" \ + "${STAGE_DIR}${RUNTIME_PREFIX}/bin/ffprobe" \ + "${STAGE_DIR}${RUNTIME_PREFIX}/bin/kodi" \ + "${STAGE_DIR}/usr/lib/systemd/system/kodi-rk.service"; do + [[ -e "${required}" ]] || die "staged runtime file is missing: ${required}" +done + +if find "${STAGE_DIR}${RUNTIME_PREFIX}" -type f -exec grep -IlF "${WORK_DIR}" {} + | grep -q .; then + die "build path leaked into a staged text file" +fi + +"${SCRIPT_DIR}/package-media.sh" \ + --stage "${STAGE_DIR}" \ + --output-dir "${OUTPUT_DIR}" \ + --version "${PACKAGE_VERSION}" + +log "media build complete" diff --git a/media/scripts/fetch-sources.sh b/media/scripts/fetch-sources.sh new file mode 100755 index 0000000..d263947 --- /dev/null +++ b/media/scripts/fetch-sources.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +# shellcheck source=lib.sh +. "${SCRIPT_DIR}/lib.sh" + +LOCK_FILE="${RKMEDIA_ROOT}/sources.lock.json" +DEST_DIR="${RKMEDIA_ROOT}/out/sources" + +usage() { + cat <<'EOF' +Usage: fetch-sources.sh [--lock FILE] [--dest DIRECTORY] + +Fetch every build input at the exact commit recorded in the source lock. +Existing repositories are accepted only when their origin and HEAD match. +EOF +} + +while (($#)); do + case "$1" in + --lock) + [[ $# -ge 2 ]] || die "--lock requires a file" + LOCK_FILE="$2" + shift 2 + ;; + --dest) + [[ $# -ge 2 ]] || die "--dest requires a directory" + DEST_DIR="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) die "unknown argument: $1" ;; + esac +done + +need_command git +need_command jq +LOCK_FILE="$(absolute_path "${LOCK_FILE}")" +DEST_DIR="$(absolute_path "${DEST_DIR}")" +[[ -f "${LOCK_FILE}" ]] || die "source lock not found: ${LOCK_FILE}" +jq -e '.schema_version == 1 and (.sources | type == "array")' "${LOCK_FILE}" >/dev/null \ + || die "invalid source lock: ${LOCK_FILE}" +mkdir -p -- "${DEST_DIR}" + +verify_checkout() { + local dir="$1" expected_repo="$2" expected_commit="$3" + local actual_repo actual_commit + [[ -d "${dir}/.git" ]] || return 1 + actual_repo="$(git -C "${dir}" remote get-url origin 2>/dev/null || true)" + actual_commit="$(git -C "${dir}" rev-parse HEAD 2>/dev/null || true)" + [[ "${actual_repo}" == "${expected_repo}" && "${actual_commit}" == "${expected_commit}" ]] +} + +while IFS=$'\t' read -r name repository ref commit; do + [[ "${name}" =~ ^[a-z0-9][a-z0-9._-]*$ ]] || die "unsafe source name in lock: ${name}" + [[ "${commit}" =~ ^[0-9a-f]{40}$ ]] || die "invalid commit for ${name}: ${commit}" + source_dir="${DEST_DIR}/${name}" + + if verify_checkout "${source_dir}" "${repository}" "${commit}"; then + log "source already verified: ${name} ${commit}" + continue + fi + if [[ -e "${source_dir}" ]]; then + die "${source_dir} exists but does not match the lock; move it aside and retry" + fi + + log "fetching ${name} at ${commit}" + mkdir -p -- "${source_dir}" + git -C "${source_dir}" init --quiet + git -C "${source_dir}" remote add origin "${repository}" + if ! git -C "${source_dir}" fetch --quiet --depth=1 origin "${commit}"; then + log "direct commit fetch was unavailable; fetching history from ${ref}" + git -C "${source_dir}" fetch --quiet --filter=blob:none origin "${ref}" + fi + git -C "${source_dir}" checkout --quiet --detach "${commit}" + verify_checkout "${source_dir}" "${repository}" "${commit}" \ + || die "checkout verification failed for ${name}" +done < <(jq -r '.sources[] | select(.build == true) | [.name, .repository, .ref, .commit] | @tsv' "${LOCK_FILE}") + +log "all locked sources are present in ${DEST_DIR}" + diff --git a/media/scripts/install-build-deps.sh b/media/scripts/install-build-deps.sh new file mode 100755 index 0000000..3ee41b1 --- /dev/null +++ b/media/scripts/install-build-deps.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +# shellcheck source=lib.sh +. "${SCRIPT_DIR}/lib.sh" + +MODE=check +case "${1:---check}" in + --check) MODE=check ;; + --install) MODE=install ;; + -h|--help) + printf 'Usage: install-build-deps.sh [--check|--install]\n' + exit 0 + ;; + *) die "unknown argument: $1" ;; +esac + +require_arm64_trixie +need_command dpkg-query + +packages=( + adduser autoconf automake autopoint autotools-dev build-essential ca-certificates + cmake default-jre devscripts dpkg-dev file gawk gcc g++ gdc gettext git gperf + hwdata jq libasound2-dev libass-dev libavahi-client-dev libavahi-common-dev + libbluetooth-dev libbluray-dev libbz2-dev libcdio++-dev libcdio-dev libcec-dev + libcrossguid-dev libcwiid-dev libcurl4-openssl-dev libdbus-1-dev libdrm-dev + libegl1-mesa-dev libenca-dev libexiv2-dev libflac-dev libfmt-dev + libfontconfig-dev libfreetype6-dev libfribidi-dev libfstrcmp-dev libgbm-dev + libgcrypt20-dev libgif-dev libgl1-mesa-dev libgles2-mesa-dev libglu1-mesa-dev + libgnutls28-dev libgpg-error-dev libgtest-dev libinput-dev libiso9660-dev + libjpeg-dev liblcms2-dev liblirc-dev libltdl-dev liblzo2-dev libmariadb-dev + libmicrohttpd-dev libnfs-dev libogg-dev libp8-platform-dev libpcre2-dev + libplist-dev libpng-dev libpulse-dev libshairplay-dev libsmbclient-dev + libspdlog-dev libsqlite3-dev libssl-dev libtag1-dev libtiff-dev + libtinyxml2-dev libtinyxml-dev libtool libudev-dev libunistring-dev + libvorbis-dev libxkbcommon-dev libxslt1-dev libxt-dev lsb-release meson nasm + ninja-build nlohmann-json3-dev patch pkg-config python3-dev python3-pil + python3-pip rapidjson-dev swig unzip uuid-dev zip zlib1g-dev +) + +missing=() +for package in "${packages[@]}"; do + if ! dpkg-query -W -f='${db:Status-Abbrev}' "${package}" 2>/dev/null | grep -q '^ii '; then + missing+=("${package}") + fi +done + +if ((${#missing[@]} == 0)); then + log "all build dependencies are installed" + exit 0 +fi + +if [[ "${MODE}" == "check" ]]; then + printf 'Missing build packages:\n' + printf ' %s\n' "${missing[@]}" + exit 1 +fi + +[[ "${EUID}" -eq 0 ]] || die "--install must run as root (use sudo)" +export DEBIAN_FRONTEND=noninteractive +apt-get update +apt-get install -y --no-install-recommends "${missing[@]}" diff --git a/media/scripts/lib.sh b/media/scripts/lib.sh new file mode 100755 index 0000000..cc0e17b --- /dev/null +++ b/media/scripts/lib.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +RKMEDIA_SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +RKMEDIA_ROOT="$(cd -- "${RKMEDIA_SCRIPT_DIR}/.." && pwd -P)" + +log() { + printf '[rkmedia] %s\n' "$*" >&2 +} + +die() { + printf '[rkmedia] ERROR: %s\n' "$*" >&2 + exit 1 +} + +need_command() { + command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" +} + +absolute_path() { + realpath -m -- "$1" +} + +assert_child_path() { + local parent child + parent="$(absolute_path "$1")" + child="$(absolute_path "$2")" + case "${child}" in + "${parent}"/*) ;; + *) die "refusing operation outside ${parent}: ${child}" ;; + esac + [[ "${child}" != "${parent}" ]] || die "refusing operation on parent directory itself: ${parent}" +} + +reset_child_dir() { + local parent="$1" + local child="$2" + assert_child_path "${parent}" "${child}" + rm -rf -- "${child}" + mkdir -p -- "${child}" +} + +require_arm64_trixie() { + local arch codename + arch="$(dpkg --print-architecture 2>/dev/null || true)" + codename="" + if [[ -r /etc/os-release ]]; then + # shellcheck disable=SC1091 + . /etc/os-release + codename="${VERSION_CODENAME:-}" + fi + + if [[ "${arch}" != "arm64" || "$(uname -m)" != "aarch64" || "${codename}" != "trixie" ]]; then + if [[ "${RKMEDIA_ALLOW_UNSUPPORTED_HOST:-0}" == "1" ]]; then + log "warning: unsupported build host (dpkg=${arch}, uname=$(uname -m), suite=${codename:-unknown})" + return + fi + die "native Debian Trixie arm64 is required (set RKMEDIA_ALLOW_UNSUPPORTED_HOST=1 only for development)" + fi +} + diff --git a/media/scripts/package-media.sh b/media/scripts/package-media.sh new file mode 100755 index 0000000..2f9890b --- /dev/null +++ b/media/scripts/package-media.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail +umask 022 +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +# shellcheck source=lib.sh +. "${SCRIPT_DIR}/lib.sh" + +STAGE_DIR="" +OUTPUT_DIR="${RKMEDIA_ROOT}/out" +PACKAGE_VERSION="$(tr -d '[:space:]' < "${RKMEDIA_ROOT}/VERSION")" +DEPENDS_FROM_DEB="" + +usage() { + printf 'Usage: package-media.sh --stage DIRECTORY [--output-dir DIRECTORY] [--version VERSION] [--depends-from-deb FILE]\n' +} + +while (($#)); do + case "$1" in + --stage) [[ $# -ge 2 ]] || die "--stage requires a directory"; STAGE_DIR="$2"; shift 2 ;; + --output-dir) [[ $# -ge 2 ]] || die "--output-dir requires a directory"; OUTPUT_DIR="$2"; shift 2 ;; + --version) [[ $# -ge 2 ]] || die "--version requires a value"; PACKAGE_VERSION="$2"; shift 2 ;; + --depends-from-deb) [[ $# -ge 2 ]] || die "--depends-from-deb requires a file"; DEPENDS_FROM_DEB="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown argument: $1" ;; + esac +done + +[[ -n "${STAGE_DIR}" ]] || die "--stage is required" +[[ "${PACKAGE_VERSION}" =~ ^[0-9A-Za-z.+:~-]+$ ]] || die "invalid Debian package version: ${PACKAGE_VERSION}" +for command_name in dpkg-deb dpkg-shlibdeps file jq md5sum sed sha256sum; do + need_command "${command_name}" +done + +STAGE_DIR="$(absolute_path "${STAGE_DIR}")" +OUTPUT_DIR="$(absolute_path "${OUTPUT_DIR}")" +[[ -x "${STAGE_DIR}/opt/rkmedia/bin/ffmpeg" ]] || die "stage does not contain the RKMedia runtime" +mkdir -p -- "${OUTPUT_DIR}" + +PACKAGE_ROOT="${OUTPUT_DIR}/package-root" +reset_child_dir "${OUTPUT_DIR}" "${PACKAGE_ROOT}" +cp -a -- "${STAGE_DIR}/." "${PACKAGE_ROOT}/" +mkdir -p -- "${PACKAGE_ROOT}/DEBIAN" "${PACKAGE_ROOT}/usr/share/doc/rk1-media-stack" + +cp -- "${RKMEDIA_ROOT}/packaging/postinst" "${PACKAGE_ROOT}/DEBIAN/postinst" +cp -- "${RKMEDIA_ROOT}/packaging/prerm" "${PACKAGE_ROOT}/DEBIAN/prerm" +cp -- "${RKMEDIA_ROOT}/packaging/postrm" "${PACKAGE_ROOT}/DEBIAN/postrm" +chmod 0755 "${PACKAGE_ROOT}/DEBIAN/postinst" "${PACKAGE_ROOT}/DEBIAN/prerm" "${PACKAGE_ROOT}/DEBIAN/postrm" +printf '/etc/rkmedia/kodi.env\n' > "${PACKAGE_ROOT}/DEBIAN/conffiles" + +# A collaborative workspace or permissive host umask must never make core +# target directories such as /etc, /usr, or /opt group-writable via dpkg. +find "${PACKAGE_ROOT}" -type d -exec chmod 0755 {} + +find "${PACKAGE_ROOT}" -type f ! -perm /0111 -exec chmod 0644 {} + +find "${PACKAGE_ROOT}" -type f -perm /0111 -exec chmod 0755 {} + +if find "${PACKAGE_ROOT}" \( -type d -o -type f \) -perm /0022 -print -quit \ + | grep -q .; then + die "package contains a group/world-writable path" +fi + +if [[ -n "${DEPENDS_FROM_DEB}" ]]; then + DEPENDS_FROM_DEB="$(absolute_path "${DEPENDS_FROM_DEB}")" + [[ -f "${DEPENDS_FROM_DEB}" ]] || die "dependency source package not found: ${DEPENDS_FROM_DEB}" + shlib_depends="$(dpkg-deb --field "${DEPENDS_FROM_DEB}" Depends)" + [[ -n "${shlib_depends}" ]] || die "upstream package has an empty Depends field" +else + analysis_root="${OUTPUT_DIR}/shlibdeps" + reset_child_dir "${OUTPUT_DIR}" "${analysis_root}" + mkdir -p -- "${analysis_root}/debian" + cat > "${analysis_root}/debian/control" <<'EOF' +Source: rk1-media-stack +Section: video +Priority: optional + +Package: rk1-media-stack +Architecture: arm64 +Description: temporary metadata for dpkg-shlibdeps +EOF + + elf_files=() + while IFS= read -r -d '' candidate; do + if file --brief "${candidate}" | grep -q '^ELF '; then + elf_files+=("${candidate}") + fi + done < <(find "${PACKAGE_ROOT}/opt/rkmedia" -type f -print0) + ((${#elf_files[@]} > 0)) || die "no ELF files found in media stage" + + pushd "${analysis_root}" >/dev/null + shlib_output="$(dpkg-shlibdeps --ignore-missing-info -O \ + -l"${PACKAGE_ROOT}/opt/rkmedia/lib" "${elf_files[@]}")" + popd >/dev/null + shlib_depends="${shlib_output#shlibs:Depends=}" + [[ -n "${shlib_depends}" && "${shlib_depends}" != "${shlib_output}" ]] \ + || die "dpkg-shlibdeps did not produce runtime dependencies" +fi + +depends="adduser, systemd, udev, ${shlib_depends}" +installed_size="$(du -sk "${PACKAGE_ROOT}" | awk '{print $1}')" +if [[ -f "${PACKAGE_ROOT}/usr/share/doc/rk1-media-stack/upstream-deb.lock.json" ]]; then + provenance_file="${PACKAGE_ROOT}/usr/share/doc/rk1-media-stack/upstream-deb.lock.json" +else + provenance_file="${PACKAGE_ROOT}/usr/share/doc/rk1-media-stack/sources.lock.json" +fi +lock_sha256="$(sha256sum "${provenance_file}" | awk '{print $1}')" + +sed \ + -e "s/@VERSION@/${PACKAGE_VERSION}/g" \ + -e "s/@INSTALLED_SIZE@/${installed_size}/g" \ + -e "s/@LOCK_SHA256@/${lock_sha256}/g" \ + -e "s/@DEPENDS@/${depends}/g" \ + "${RKMEDIA_ROOT}/packaging/control.in" > "${PACKAGE_ROOT}/DEBIAN/control" + +pushd "${PACKAGE_ROOT}" >/dev/null +find etc opt usr -type f -print0 \ + | sort -z \ + | xargs -0 md5sum > DEBIAN/md5sums +popd >/dev/null + +if find "${PACKAGE_ROOT}" \( -type d -o -type f \) -perm /0022 -print -quit \ + | grep -q .; then + die "package contains a group/world-writable path after metadata generation" +fi + +output_deb="${OUTPUT_DIR}/rk1-media-stack_${PACKAGE_VERSION}_arm64.deb" +rm -f -- "${output_deb}" +SOURCE_DATE_EPOCH="$(jq -er '.source_date_epoch' "${RKMEDIA_ROOT}/sources.lock.json")" \ + dpkg-deb --root-owner-group --build "${PACKAGE_ROOT}" "${output_deb}" +( + cd "${OUTPUT_DIR}" + sha256sum "$(basename -- "${output_deb}")" \ + >"$(basename -- "${output_deb}").sha256" +) +dpkg-deb --info "${output_deb}" >/dev/null +log "created ${output_deb}" diff --git a/media/scripts/repack-upstream-deb.sh b/media/scripts/repack-upstream-deb.sh new file mode 100755 index 0000000..3c958e8 --- /dev/null +++ b/media/scripts/repack-upstream-deb.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +# shellcheck source=lib.sh +. "${SCRIPT_DIR}/lib.sh" + +LOCK_FILE="${RKMEDIA_ROOT}/upstream-deb.lock.json" +WORK_DIR="${RKMEDIA_ROOT}/out/repack-work" +OUTPUT_DIR="${RKMEDIA_ROOT}/out" +CACHE_DIR="${RKMEDIA_ROOT}/out/downloads" +INPUT_DEB="" +PACKAGE_VERSION="20260721.1741+rk1.1" + +usage() { + cat <<'EOF' +Usage: repack-upstream-deb.sh [options] + + --deb FILE Use an existing upstream .deb instead of downloading + --work-dir DIR Temporary extraction directory + --output-dir DIR Repacked package output directory + --cache-dir DIR Download cache directory + --version VERSION Output Debian package version +EOF +} + +while (($#)); do + case "$1" in + --deb) [[ $# -ge 2 ]] || die "--deb requires a file"; INPUT_DEB="$2"; shift 2 ;; + --work-dir) [[ $# -ge 2 ]] || die "--work-dir requires a directory"; WORK_DIR="$2"; shift 2 ;; + --output-dir) [[ $# -ge 2 ]] || die "--output-dir requires a directory"; OUTPUT_DIR="$2"; shift 2 ;; + --cache-dir) [[ $# -ge 2 ]] || die "--cache-dir requires a directory"; CACHE_DIR="$2"; shift 2 ;; + --version) [[ $# -ge 2 ]] || die "--version requires a value"; PACKAGE_VERSION="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown argument: $1" ;; + esac +done + +for command_name in curl dpkg-deb file jq readelf sha256sum stat; do + need_command "${command_name}" +done +[[ -f "${LOCK_FILE}" ]] || die "upstream asset lock is missing: ${LOCK_FILE}" +jq -e '.schema_version == 1 and (.asset_sha256 | test("^[0-9a-f]{64}$"))' "${LOCK_FILE}" >/dev/null \ + || die "invalid upstream asset lock" + +WORK_DIR="$(absolute_path "${WORK_DIR}")" +OUTPUT_DIR="$(absolute_path "${OUTPUT_DIR}")" +CACHE_DIR="$(absolute_path "${CACHE_DIR}")" +mkdir -p -- "${WORK_DIR}" "${OUTPUT_DIR}" "${CACHE_DIR}" + +asset_name="$(jq -er '.asset_name' "${LOCK_FILE}")" +asset_url="$(jq -er '.asset_url' "${LOCK_FILE}")" +expected_size="$(jq -er '.asset_size' "${LOCK_FILE}")" +expected_sha256="$(jq -er '.asset_sha256' "${LOCK_FILE}")" + +if [[ -z "${INPUT_DEB}" ]]; then + INPUT_DEB="${CACHE_DIR}/${asset_name}" + if [[ ! -f "${INPUT_DEB}" ]]; then + partial="${INPUT_DEB}.partial" + rm -f -- "${partial}" + log "downloading pinned upstream Kodi package" + curl -L --fail --show-error --output "${partial}" "${asset_url}" + mv -- "${partial}" "${INPUT_DEB}" + fi +fi +INPUT_DEB="$(absolute_path "${INPUT_DEB}")" +[[ -f "${INPUT_DEB}" ]] || die "upstream package not found: ${INPUT_DEB}" + +actual_size="$(stat -c '%s' "${INPUT_DEB}")" +actual_sha256="$(sha256sum "${INPUT_DEB}" | awk '{print $1}')" +[[ "${actual_size}" == "${expected_size}" ]] || die "upstream package size mismatch" +[[ "${actual_sha256}" == "${expected_sha256}" ]] || die "upstream package SHA-256 mismatch" +[[ "$(dpkg-deb --field "${INPUT_DEB}" Package)" == "$(jq -er '.package' "${LOCK_FILE}")" ]] \ + || die "upstream package name mismatch" +[[ "$(dpkg-deb --field "${INPUT_DEB}" Version)" == "$(jq -er '.package_version' "${LOCK_FILE}")" ]] \ + || die "upstream package version mismatch" +[[ "$(dpkg-deb --field "${INPUT_DEB}" Architecture)" == "arm64" ]] \ + || die "upstream package is not arm64" + +STAGE_DIR="${WORK_DIR}/stage" +reset_child_dir "${WORK_DIR}" "${STAGE_DIR}" +dpkg-deb --extract "${INPUT_DEB}" "${STAGE_DIR}" + +local_bin="${STAGE_DIR}/usr/local/bin" +local_lib="${STAGE_DIR}/usr/local/lib" +runtime_bin="${STAGE_DIR}/opt/rkmedia/bin" +runtime_lib="${STAGE_DIR}/opt/rkmedia/lib" +mkdir -p -- "${runtime_bin}" "${runtime_lib}" + +for program in ffmpeg ffprobe; do + [[ -x "${local_bin}/${program}" ]] || die "upstream package is missing ${program}" + mv -- "${local_bin}/${program}" "${runtime_bin}/${program}" +done +if [[ -x "${local_bin}/di-edid-decode" ]]; then + mv -- "${local_bin}/di-edid-decode" "${runtime_bin}/di-edid-decode" +fi + +shopt -s nullglob +runtime_libraries=( + "${local_lib}"/libdav1d.so* + "${local_lib}"/librga.so* + "${local_lib}"/librockchip_mpp.so* + "${local_lib}"/librockchip_vpu.so* + "${local_lib}"/aarch64-linux-gnu/libdisplay-info.so* +) +((${#runtime_libraries[@]} > 0)) || die "no Rockchip runtime libraries found in upstream package" +mv -- "${runtime_libraries[@]}" "${runtime_lib}/" +shopt -u nullglob + +find "${local_lib}" -type f \( -name '*.a' -o -name '*.la' \) -delete +rm -rf -- "${local_lib}/pkgconfig" "${local_lib}/aarch64-linux-gnu/pkgconfig" +rm -f -- \ + "${STAGE_DIR}/usr/lib/systemd/system/kodi.service" \ + "${STAGE_DIR}/usr/lib/systemd/system/kodi-pulse.service" \ + "${STAGE_DIR}/usr/lib/systemd/system/pulseaudio.service" + +cp -a -- "${RKMEDIA_ROOT}/packaging/rootfs/." "${STAGE_DIR}/" +mkdir -p -- "${STAGE_DIR}/usr/bin" "${STAGE_DIR}/usr/share/doc/rk1-media-stack" +ln -sfn /opt/rkmedia/bin/ffmpeg-rk "${STAGE_DIR}/usr/bin/ffmpeg-rk" +ln -sfn /opt/rkmedia/bin/ffprobe-rk "${STAGE_DIR}/usr/bin/ffprobe-rk" +ln -sfn /opt/rkmedia/bin/kodi-rk "${STAGE_DIR}/usr/bin/kodi-rk" +install -m 0644 "${LOCK_FILE}" "${STAGE_DIR}/usr/share/doc/rk1-media-stack/upstream-deb.lock.json" + +[[ -x "${STAGE_DIR}/usr/local/bin/kodi" ]] || die "Kodi launcher was not retained at its compiled prefix" +[[ -x "${STAGE_DIR}/usr/local/lib/kodi/kodi-gbm" ]] || die "Kodi GBM binary is missing" +for soname in librga.so.2 libdav1d.so.7 librockchip_mpp.so.1 libdisplay-info.so.4; do + [[ -e "${runtime_lib}/${soname}" ]] || die "relocated runtime is missing ${soname}" + readelf -d "${STAGE_DIR}/usr/local/lib/kodi/kodi-gbm" | grep -Fq "Shared library: [${soname}]" \ + || die "Kodi binary does not declare expected dependency ${soname}" +done +if find "${STAGE_DIR}" -type f -name '*.a' -print -quit | grep -q .; then + die "static library remained after repack" +fi +if find "${STAGE_DIR}/usr/local/lib" -type d -name pkgconfig -print -quit | grep -q .; then + die "pkg-config development directory remained after repack" +fi + +"${SCRIPT_DIR}/package-media.sh" \ + --stage "${STAGE_DIR}" \ + --output-dir "${OUTPUT_DIR}" \ + --version "${PACKAGE_VERSION}" \ + --depends-from-deb "${INPUT_DEB}" + +log "repack complete; authoritative input SHA-256: ${actual_sha256}" diff --git a/media/sources.lock.json b/media/sources.lock.json new file mode 100644 index 0000000..0f1a2bf --- /dev/null +++ b/media/sources.lock.json @@ -0,0 +1,98 @@ +{ + "schema_version": 1, + "generated_at": "2026-08-16T16:00:00Z", + "source_date_epoch": 1786886768, + "target": { + "architecture": "arm64", + "gnu_machine": "aarch64-linux-gnu", + "distribution": "debian", + "suite": "trixie", + "install_prefix": "/opt/rkmedia" + }, + "sources": [ + { + "name": "mpp", + "description": "Rockchip MPP compatibility branch used by ffmpeg-rockchip and kodi-rockchip-deb", + "repository": "https://github.com/nyanmisaka/mpp.git", + "ref": "refs/heads/jellyfin-mpp", + "commit": "a9380ef333102ac318628f83b5f7a460d377749e", + "committed_at": "2025-12-26T12:59:40Z", + "build": true, + "license_files": ["LICENSES"] + }, + { + "name": "rga", + "description": "Rockchip RGA compatibility branch used by ffmpeg-rockchip and kodi-rockchip-deb", + "repository": "https://github.com/nyanmisaka/rk-mirrors.git", + "ref": "refs/heads/jellyfin-rga", + "commit": "1d330cc28551943bed3380261a5a9c6fbd58ff53", + "committed_at": "2025-10-19T08:11:31Z", + "build": true, + "license_files": ["COPYING"] + }, + { + "name": "dav1d", + "description": "AV1 software fallback required by the matched FFmpeg/Kodi build", + "repository": "https://code.videolan.org/videolan/dav1d.git", + "ref": "refs/tags/1.5.3", + "commit": "b546257f770768b2c88258c533da38b91a06f737", + "committed_at": "2025-12-31T14:50:45Z", + "build": true, + "license_files": ["COPYING"] + }, + { + "name": "ffmpeg-rockchip", + "description": "FFmpeg 8.1 with RKMPP decoders/encoders and RKRGA filters", + "repository": "https://github.com/nyanmisaka/ffmpeg-rockchip.git", + "ref": "refs/heads/8.1", + "commit": "d90e3a1c18d7929383cf88c1b3da2e2d1c966cbf", + "committed_at": "2026-08-10T08:42:00Z", + "build": true, + "license_files": ["COPYING.GPLv2", "COPYING.GPLv3", "COPYING.LGPLv2.1", "COPYING.LGPLv3"] + }, + { + "name": "libdisplay-info", + "description": "EDID and DisplayID parser required by Kodi's GBM platform", + "repository": "https://gitlab.freedesktop.org/emersion/libdisplay-info.git", + "ref": "refs/heads/main", + "commit": "f1b75310181e364f97a16464efcd92bc82e57ccc", + "committed_at": "2026-07-27T10:12:35Z", + "build": true, + "license_files": ["LICENSE"] + }, + { + "name": "kodi", + "description": "Kodi mainline with current GBM DRM PRIME support", + "repository": "https://github.com/xbmc/xbmc.git", + "ref": "refs/heads/master", + "commit": "9c56bf593441a6c246fdc6931177e71f349ee2bc", + "committed_at": "2026-08-16T13:26:08Z", + "build": true, + "license_files": ["LICENSE.md"] + } + ], + "reference_heads": [ + { + "name": "kodi-rockchip-deb", + "repository": "https://github.com/armsurvivors/kodi-rockchip-deb.git", + "ref": "refs/heads/main", + "commit": "8540dbd24f180aa43cf2d6e55f2ad17bd4f12530", + "observed_at": "2026-08-16T16:00:00Z" + }, + { + "name": "rockchip-linux-mpp-official", + "repository": "https://github.com/rockchip-linux/mpp.git", + "ref": "refs/heads/develop", + "commit": "c08762ebfadeb4e986d2fed993bc7a54862d3ebe", + "observed_at": "2026-08-16T16:00:00Z" + }, + { + "name": "airockchip-librga-official", + "repository": "https://github.com/airockchip/librga.git", + "ref": "refs/heads/main", + "commit": "2b32edcb97b601b25683e2941d888c8515da6d55", + "observed_at": "2026-08-16T16:00:00Z" + } + ] +} + diff --git a/media/upstream-deb.lock.json b/media/upstream-deb.lock.json new file mode 100644 index 0000000..847d97f --- /dev/null +++ b/media/upstream-deb.lock.json @@ -0,0 +1,17 @@ +{ + "schema_version": 1, + "release": "20260721-1741", + "release_url": "https://github.com/armsurvivors/kodi-rockchip-deb/releases/tag/20260721-1741", + "recipe_repository": "https://github.com/armsurvivors/kodi-rockchip-deb.git", + "recipe_commit": "4ceb39b453dbbb1de13bd355e2be2542427e2b1e", + "asset_id": 484995753, + "asset_name": "kodi-rockchip-gbm_arm64_kodi_master_ffmpeg_81_trixie.deb", + "asset_url": "https://github.com/armsurvivors/kodi-rockchip-deb/releases/download/20260721-1741/kodi-rockchip-gbm_arm64_kodi_master_ffmpeg_81_trixie.deb", + "asset_size": 147977956, + "asset_sha256": "f9eed4c02d80f12b443604026fb00c4a6ff5ddd1fe5e6c554d81852a849d261a", + "package": "kodi-rockchip-gbm", + "package_version": "20260721-1741-kodi-master-ffmpeg-81", + "architecture": "arm64", + "embedded_ffmpeg_revision": "388741a", + "notes": "The release recipe cloned moving branches without commit locks. The GitHub asset SHA-256 is therefore the authoritative immutable input." +} diff --git a/packages/debian-packages.lock.tsv b/packages/debian-packages.lock.tsv new file mode 100644 index 0000000..ec6c882 --- /dev/null +++ b/packages/debian-packages.lock.tsv @@ -0,0 +1,141 @@ +# filename size sha256 url +avahi-daemon_0.8-16_arm64.deb 80636 c88a5d29eefafecdfe52f1357898e0f44e9c0b0a88b62df8311fe3c836b836cd https://deb.debian.org/debian/pool/main/a/avahi/avahi-daemon_0.8-16_arm64.deb +cec-utils_7.0.0-1+b1_arm64.deb 31016 516d5d5df5002dd470d6cea1ea49626765be697815c08c9083ccbc9040f1fe46 https://deb.debian.org/debian/pool/main/libc/libcec/cec-utils_7.0.0-1+b1_arm64.deb +cifs-utils_2%3a7.4-1_arm64.deb 98004 b80ad8586843b5ca439829f95d37201a13c80ed73a0d7bdfffebd42683c38ace https://deb.debian.org/debian/pool/main/c/cifs-utils/cifs-utils_7.4-1_arm64.deb +distro-info-data_0.66+deb13u2_all.deb 6792 91bf35d6efa2a12e0d81abea1819e87fef2d292899f41c40a7ee1085e92cba60 https://deb.debian.org/debian/pool/main/d/distro-info-data/distro-info-data_0.66+deb13u2_all.deb +fontconfig-config_2.15.0-2.3_arm64.deb 318304 10f643f467b51cf25fc18d1945bc61246dd8d8d1fe6bc712a2b1712c74ebc4d6 https://deb.debian.org/debian/pool/main/f/fontconfig/fontconfig-config_2.15.0-2.3_arm64.deb +fonts-dejavu-core_2.37-8_all.deb 840460 86635b3d25b3655fc11cb3ecc3af59f0bf19643b02b94f2de48bd10253cdba12 https://deb.debian.org/debian/pool/main/f/fonts-dejavu/fonts-dejavu-core_2.37-8_all.deb +fonts-dejavu-mono_2.37-8_all.deb 488808 3003e98a5debfdeadc7040a7f715fe9fe6fb67f68deacf6049b54e30f07fc014 https://deb.debian.org/debian/pool/main/f/fonts-dejavu/fonts-dejavu-mono_2.37-8_all.deb +ir-keytable_1.30.1-1_arm64.deb 156240 f70c3afc00fb2fcea6c4fabe24135f110b3642a195e09764202759bfa948e1d7 https://deb.debian.org/debian/pool/main/v/v4l-utils/ir-keytable_1.30.1-1_arm64.deb +iso-codes_4.18.0-1_all.deb 3124584 969582afb02fd3f0833a666b2112931a999c4f6b23e7fe514f8051dfd7672a45 https://deb.debian.org/debian/pool/main/i/iso-codes/iso-codes_4.18.0-1_all.deb +keyutils_1.6.3-6_arm64.deb 54264 21a2ceadbb2f42d9a176646164e573035bcbcb43c1bdfff5a496decd189b459f https://deb.debian.org/debian/pool/main/k/keyutils/keyutils_1.6.3-6_arm64.deb +kmscube_0.0.0~git20210103-1+b3_arm64.deb 75800 5a3549ae3a3b24ed0307b7b8e5ac05878447ae4053d25941bc224fa43b4cdab1 https://deb.debian.org/debian/pool/main/k/kmscube/kmscube_0.0.0~git20210103-1+b3_arm64.deb +libass9_1%3a0.17.3-1+deb13u1_arm64.deb 101336 849fbb86d5fa8b3cac38b80ff577ff3d1f85e6fdcb95313db4639593069843e2 https://deb.debian.org/debian/pool/main/liba/libass/libass9_0.17.3-1+deb13u1_arm64.deb +libasyncns0_0.8-6+b5_arm64.deb 11672 818bbcaa7ada536a1bdd5b1c26b77608606403f5c15557691e83d0b4433b89ef https://deb.debian.org/debian/pool/main/liba/libasyncns/libasyncns0_0.8-6+b5_arm64.deb +libavahi-client3_0.8-16_arm64.deb 46740 aad7c1a4f0e8b9131881ebdd61150bb9e15f11534aafe4877e61fefc266f1586 https://deb.debian.org/debian/pool/main/a/avahi/libavahi-client3_0.8-16_arm64.deb +libavahi-common-data_0.8-16_arm64.deb 112288 75d80488f931f6cc577aaf366b1a17099bd4d0c179dfd33d014f564094255da9 https://deb.debian.org/debian/pool/main/a/avahi/libavahi-common-data_0.8-16_arm64.deb +libavahi-common3_0.8-16_arm64.deb 43340 495d3502dbaed9e71c54cd0030fd135d62140b7c780620cc14a0a8a615adda14 https://deb.debian.org/debian/pool/main/a/avahi/libavahi-common3_0.8-16_arm64.deb +libavahi-core7_0.8-16_arm64.deb 98280 1d60d72c3c4c3a59d86c9b681dbbb71f1ea80c1ea754327a41c96a7288a01f5d https://deb.debian.org/debian/pool/main/a/avahi/libavahi-core7_0.8-16_arm64.deb +libbluetooth3_5.82-1.1_arm64.deb 103028 67395cec3551163c302889cf2eeea12a11de6c069aff8e7a1b9348770a24e180 https://deb.debian.org/debian/pool/main/b/bluez/libbluetooth3_5.82-1.1_arm64.deb +libbluray2_1%3a1.3.4-1+b2_arm64.deb 125560 14da09203931bdbff17b129d447850daecc8c72b87b296e3f7d0e0beb56e9f50 https://deb.debian.org/debian/pool/main/libb/libbluray/libbluray2_1.3.4-1+b2_arm64.deb +libcdio19t64_2.2.0-4.1~deb13u1_arm64.deb 59316 3c76de181f7018ca14e29f75ba6992cf5ec9099e3c3ee304d9d64454af48062f https://deb.debian.org/debian/pool/main/libc/libcdio/libcdio19t64_2.2.0-4.1~deb13u1_arm64.deb +libcec7_7.0.0-1+b1_arm64.deb 189744 f9a9042af0fe627eabf4a4d4a8467a6b0bbeceae1b772f86b5027364f9bae6cf https://deb.debian.org/debian/pool/main/libc/libcec/libcec7_7.0.0-1+b1_arm64.deb +libdaemon0_0.14-7.1+b3_arm64.deb 13152 a685244841cd0840b269da6eeac476410c454b74f1c265823073d27c91a4cefb https://deb.debian.org/debian/pool/main/libd/libdaemon/libdaemon0_0.14-7.1+b3_arm64.deb +libdecor-0-0_0.2.2-2_arm64.deb 15204 34c0db59390767ef7b1cb5594f78f142b0673a5e7788e068cdd323f9c16f9207 https://deb.debian.org/debian/pool/main/libd/libdecor-0/libdecor-0-0_0.2.2-2_arm64.deb +libdrm-amdgpu1_2.4.124-2_arm64.deb 21828 e36c72774cc215818b48ee2174c7ad1d9a2bf89a09cdee7a7a128d288d8eef43 https://deb.debian.org/debian/pool/main/libd/libdrm/libdrm-amdgpu1_2.4.124-2_arm64.deb +libdrm-common_2.4.124-2_all.deb 8288 9a8a6c65c165e9964f106fb4ac710959b5d33e0790227e3ab6b27c4742d1254a https://deb.debian.org/debian/pool/main/libd/libdrm/libdrm-common_2.4.124-2_all.deb +libdrm2_2.4.124-2_arm64.deb 38304 b535506630bb6a9a616bc07caf3b29915f980d8c3437852fb96ea64fb1a8a3f8 https://deb.debian.org/debian/pool/main/libd/libdrm/libdrm2_2.4.124-2_arm64.deb +libegl-mesa0_25.0.7-2+deb13u1_arm64.deb 120960 e344e0c87657d91ebfcb059bee3a3c87ba05871e2918079c3f9393665d4472af https://deb.debian.org/debian/pool/main/m/mesa/libegl-mesa0_25.0.7-2+deb13u1_arm64.deb +libegl1_1.7.0-1+b2_arm64.deb 33952 b663ad1777df7ed8e320f897bbf678c2baef67f5458fe4a43195ac9b444c7afb https://deb.debian.org/debian/pool/main/libg/libglvnd/libegl1_1.7.0-1+b2_arm64.deb +libevdev2_1.13.4+dfsg-1_arm64.deb 30356 abdb9ca2f0a27d88ec16b480f346d1d6c183c59e3d071734ba8d8b9794aacfc6 https://deb.debian.org/debian/pool/main/libe/libevdev/libevdev2_1.13.4+dfsg-1_arm64.deb +libevent-core-2.1-7t64_2.1.12-stable-10+b1_arm64.deb 125752 84a27a752d520df78bab24a499f66db21d5274803eb9a2e4d7f88554210f7dea https://deb.debian.org/debian/pool/main/libe/libevent/libevent-core-2.1-7t64_2.1.12-stable-10+b1_arm64.deb +libexiv2-28_0.28.5+dfsg-1_arm64.deb 820348 62a590d7d09f4ef23acb53abe5f0be2027f71f73cda8c869e812f3fd17f8a1b7 https://deb.debian.org/debian/pool/main/e/exiv2/libexiv2-28_0.28.5+dfsg-1_arm64.deb +libexiv2-data_0.28.5+dfsg-1_all.deb 1775296 afc70f25e5223489d00347bc28f7eb05cf56f6b3faaf05a506ce6d0efbd5fa61 https://deb.debian.org/debian/pool/main/e/exiv2/libexiv2-data_0.28.5+dfsg-1_all.deb +libflac14_1.5.0+ds-2_arm64.deb 179768 11800827306e56fcbd1a65ea67d9d13126fc02bf87fdfecd35e0f48a145b3c59 https://deb.debian.org/debian/pool/main/f/flac/libflac14_1.5.0+ds-2_arm64.deb +libfontconfig1_2.15.0-2.3_arm64.deb 387092 872781cf62926b68c34a4302a3caae334ff8d4d83a2b2477f8fb454460aadc99 https://deb.debian.org/debian/pool/main/f/fontconfig/libfontconfig1_2.15.0-2.3_arm64.deb +libfreetype6_2.13.3+dfsg-1+deb13u1_arm64.deb 422284 0c426e83f1af816b9a4df9056fd2238463d1cd07969ee640965da9fb6ecaf107 https://deb.debian.org/debian/pool/main/f/freetype/libfreetype6_2.13.3+dfsg-1+deb13u1_arm64.deb +libfribidi0_1.0.16-1_arm64.deb 26460 5f5bfdf6ef126b0a38f8ebb8751cbbb845fe2cd4e1dd5258d241be243e09897f https://deb.debian.org/debian/pool/main/f/fribidi/libfribidi0_1.0.16-1_arm64.deb +libfstrcmp0_0.7.D001-5_arm64.deb 7964 bf36ecf2c4d18a1e8cd55d5a9db5f495bd4ef482778d686a49ddaa2c0cdadc60 https://deb.debian.org/debian/pool/main/f/fstrcmp/libfstrcmp0_0.7.D001-5_arm64.deb +libgbm1_25.0.7-2+deb13u1_arm64.deb 44144 2beb8d20ca715a01b7b2352c1167c0d7097c9d8a486704a9a966f95d4077cdfc https://deb.debian.org/debian/pool/main/m/mesa/libgbm1_25.0.7-2+deb13u1_arm64.deb +libgif7_5.2.2-1+deb13u1_arm64.deb 44120 658318d55d788a628836d35d994f60057d2655cb81b355648fbe2dc3003ddeb9 https://deb.debian.org/debian/pool/main/g/giflib/libgif7_5.2.2-1+deb13u1_arm64.deb +libgl1-mesa-dri_25.0.7-2+deb13u1_arm64.deb 45852 628245ff9d8e4f11edb66c0854915018402853e0eb588343d472111f6c4fb582 https://deb.debian.org/debian/pool/main/m/mesa/libgl1-mesa-dri_25.0.7-2+deb13u1_arm64.deb +libgl1_1.7.0-1+b2_arm64.deb 90876 7943f99962aa3316515760bf44e66f0c15a3c813ad750b784b43e319fa2e6003 https://deb.debian.org/debian/pool/main/libg/libglvnd/libgl1_1.7.0-1+b2_arm64.deb +libgles2_1.7.0-1+b2_arm64.deb 18036 c727fa1746ed3e343d1a2917932363d71e961eacca0443a1e2296587e03c47fb https://deb.debian.org/debian/pool/main/libg/libglvnd/libgles2_1.7.0-1+b2_arm64.deb +libglvnd0_1.7.0-1+b2_arm64.deb 41588 29ec7b8cf1c9a47055de643eb0a1921854e762b4789a7f3bc3b0dea498fd4da9 https://deb.debian.org/debian/pool/main/libg/libglvnd/libglvnd0_1.7.0-1+b2_arm64.deb +libglx-mesa0_25.0.7-2+deb13u1_arm64.deb 142320 18c02afcdd88f547ba66d3ae9290f93ab155381bf8d96b0127f9028374acfc64 https://deb.debian.org/debian/pool/main/m/mesa/libglx-mesa0_25.0.7-2+deb13u1_arm64.deb +libglx0_1.7.0-1+b2_arm64.deb 31080 0245aafaac5e68ca9511f23c5bebf010327e63c65790c03dbb80bf14d485956d https://deb.debian.org/debian/pool/main/libg/libglvnd/libglx0_1.7.0-1+b2_arm64.deb +libgraphite2-3_1.3.14-2+deb13u1_arm64.deb 70348 b0c3414b113de62be1dc28fa2c899ac0856256e2f094d92c36c3275a5053311a https://deb.debian.org/debian/pool/main/g/graphite2/libgraphite2-3_1.3.14-2+deb13u1_arm64.deb +libgstreamer-plugins-base1.0-0_1.26.2-1+deb13u1_arm64.deb 897352 bc3205ea9fd35448b2cb6171da0e24bd3af893fb8a01f8eb797cbfb4d1fe7b0e https://deb.debian.org/debian/pool/main/g/gst-plugins-base1.0/libgstreamer-plugins-base1.0-0_1.26.2-1+deb13u1_arm64.deb +libgstreamer1.0-0_1.26.2-2_arm64.deb 1294408 77d44e8bc93793f815ea7b8cdb9c1b0b0bf6fd5797e2e69a7ef95bb37f249a86 https://deb.debian.org/debian/pool/main/g/gstreamer1.0/libgstreamer1.0-0_1.26.2-2_arm64.deb +libgudev-1.0-0_238-6_arm64.deb 13720 f0fec02f1bbc0803c9d044413609d3cd0fde24bfe6a5141a38aabfa86c292b21 https://deb.debian.org/debian/pool/main/libg/libgudev/libgudev-1.0-0_238-6_arm64.deb +libharfbuzz0b_10.2.0-1+deb13u1_arm64.deb 441932 3ff928f981b54b491ee12e4cf68558cc5d61c652bdab276cd4c4c3fbbcee16ca https://deb.debian.org/debian/pool/main/h/harfbuzz/libharfbuzz0b_10.2.0-1+deb13u1_arm64.deb +libicu76_76.1-4_arm64.deb 9523672 62eb15577c881b7d02aa861e3eb860902335630cc8174daf525b48c2fa39298f https://deb.debian.org/debian/pool/main/i/icu/libicu76_76.1-4_arm64.deb +libinih1_59-1_arm64.deb 6932 fcbc936af8fdd3db56b2a8fc4891b27955ce48c7639cbd6905fd292ae20061fa https://deb.debian.org/debian/pool/main/libi/libinih/libinih1_59-1_arm64.deb +libinireader0_59-1_arm64.deb 12188 40937f22b74c8ac9bdb5f286b53ea7b81eb543d1d0497854412d0dd664e0c626 https://deb.debian.org/debian/pool/main/libi/libinih/libinireader0_59-1_arm64.deb +libinput-bin_1.28.1-1+deb13u1_arm64.deb 26788 9da373d042dd8e0c45e34a5a0dc361cb386fd5ff4f066b7b3e64e1ea66de0835 https://deb.debian.org/debian/pool/main/libi/libinput/libinput-bin_1.28.1-1+deb13u1_arm64.deb +libinput10_1.28.1-1+deb13u1_arm64.deb 129616 a2acd04f3b1e9e91d22a26b63650087a179b6b3dbaddadc92d89edfbb2075ae1 https://deb.debian.org/debian/pool/main/libi/libinput/libinput10_1.28.1-1+deb13u1_arm64.deb +libjansson4_2.14-2+b3_arm64.deb 39228 7938472b1ddfa8b0c8f58d5f44406ae7a77a342dbb502a02f6bf292b4f853ab0 https://deb.debian.org/debian/pool/main/j/jansson/libjansson4_2.14-2+b3_arm64.deb +libjpeg62-turbo_1%3a2.1.5-4_arm64.deb 172784 e4989073bb0bac8a6ec043c7adb80e1dfe601d8552233da48bb24ab45d1a1d4a https://deb.debian.org/debian/pool/main/libj/libjpeg-turbo/libjpeg62-turbo_2.1.5-4_arm64.deb +liblcms2-2_2.16-2+deb13u2_arm64.deb 151540 6636997fc488327a8d0951a038df6e5178a52b3f5e5cc2386d42655621f5672d https://deb.debian.org/debian/pool/main/l/lcms2/liblcms2-2_2.16-2+deb13u2_arm64.deb +libldb2_2%3a2.11.0+samba4.22.10+dfsg-0+deb13u2_arm64.deb 172588 b3017ddb7b56ca01c3501f6a25c663bb45747e01e5de5c8341f853fb42d80cf1 https://security.debian.org/debian-security/pool/updates/main/s/samba/libldb2_2.11.0+samba4.22.10+dfsg-0+deb13u2_arm64.deb +liblirc-client0t64_0.10.2-0.10_arm64.deb 30864 ddde026178705ec2227c603b93121bd8f35d2d8231c9db269de3127df98075b1 https://deb.debian.org/debian/pool/main/l/lirc/liblirc-client0t64_0.10.2-0.10_arm64.deb +libllvm19_1%3a19.1.7-3+b1_arm64.deb 23285344 8a48e1a49f7fcc3be74e697767d72902f4b03041401d1529bcfad1849df39148 https://deb.debian.org/debian/pool/main/l/llvm-toolchain-19/libllvm19_19.1.7-3+b1_arm64.deb +liblmdb0_0.9.31-1+b2_arm64.deb 43504 aaef40a5569a13235e4b067b07e170130fc50c5ff21d685e8961290323baff1d https://deb.debian.org/debian/pool/main/l/lmdb/liblmdb0_0.9.31-1+b2_arm64.deb +libmariadb3_1%3a11.8.6-0+deb13u1_arm64.deb 175040 29acd0080b3ea32440a8ebd0445bb13e88ff564c408faf2bfb70c129c2048503 https://deb.debian.org/debian/pool/main/m/mariadb/libmariadb3_11.8.6-0+deb13u1_arm64.deb +libmicrohttpd12t64_1.0.1-4_arm64.deb 147824 c6822d190185155f25eec4ab42613b47d68ee76811415c563e88ce3a35fcf179 https://deb.debian.org/debian/pool/main/libm/libmicrohttpd/libmicrohttpd12t64_1.0.1-4_arm64.deb +libmp3lame0_3.100-6+b3_arm64.deb 354760 ed3563722129ffa4def03b7af0b589fa20eef972bdcc4599202ddb1ac654c58e https://deb.debian.org/debian/pool/main/l/lame/libmp3lame0_3.100-6+b3_arm64.deb +libmpg123-0t64_1.32.10-1+deb13u1_arm64.deb 143208 d284df39ff3b64f1cdf274352613c52534c55eed82c3a2ff0fc7ba154acd7bf0 https://deb.debian.org/debian/pool/main/m/mpg123/libmpg123-0t64_1.32.10-1+deb13u1_arm64.deb +libmtdev1t64_1.1.7-1_arm64.deb 22456 3433f77a9f96d0dc537e870db734a4dbb50528836782c86709845736e8897b47 https://deb.debian.org/debian/pool/main/m/mtdev/libmtdev1t64_1.1.7-1_arm64.deb +libncurses6_6.5+20250216-2_arm64.deb 96112 259fe5736ebb105df7c6a7b5cc6231a8743666f5f57885f6c7989babe2fc0082 https://deb.debian.org/debian/pool/main/n/ncurses/libncurses6_6.5+20250216-2_arm64.deb +libnfs14_5.0.2-1+b2_arm64.deb 102472 1929f3b75a32c99582e55039a91c4be93561be9bcf652122e602d5098fe51221 https://deb.debian.org/debian/pool/main/libn/libnfs/libnfs14_5.0.2-1+b2_arm64.deb +libnfsidmap1_1%3a2.8.3-1_arm64.deb 55728 861f226ebe6d1ea4c7951b92e900b1a92902d8c897c902535a67579877c4ef54 https://deb.debian.org/debian/pool/main/n/nfs-utils/libnfsidmap1_2.8.3-1_arm64.deb +libogg0_1.3.5-3+b2_arm64.deb 23580 d59a83ab352bf0c16ce9513fdf264f87214813ca8c628c652801ef476ece8286 https://deb.debian.org/debian/pool/main/libo/libogg/libogg0_1.3.5-3+b2_arm64.deb +libopus0_1.5.2-2_arm64.deb 2812952 1d980e8a4717074805a98dbe07671cb3bee068c25709315f3de836980c629836 https://deb.debian.org/debian/pool/main/o/opus/libopus0_1.5.2-2_arm64.deb +liborc-0.4-0t64_1%3a0.4.41-1_arm64.deb 170796 e69ee68606246657ff19bfe13058a9aef177e686b97638728f9e0e911af07680 https://deb.debian.org/debian/pool/main/o/orc/liborc-0.4-0t64_0.4.41-1_arm64.deb +libp8-platform2_2.1.0.1+dfsg1-4+b3_arm64.deb 20216 d3e7af8fd96a2532600260f35421f05564fcde3a2aaf6355d2500cd212c03e20 https://deb.debian.org/debian/pool/main/p/p8-platform/libp8-platform2_2.1.0.1+dfsg1-4+b3_arm64.deb +libpci3_1%3a3.13.0-2_arm64.deb 71808 fde73536f93fcd930362531506f8431b2e959bfbdeb41e772100c1cfc1f83953 https://deb.debian.org/debian/pool/main/p/pciutils/libpci3_3.13.0-2_arm64.deb +libplist-2.0-4_2.6.0-2+b1_arm64.deb 46824 45d585abe7d0ba46abda543513d18869a14d21e4abba266b851dd71f2f69882b https://deb.debian.org/debian/pool/main/libp/libplist/libplist-2.0-4_2.6.0-2+b1_arm64.deb +libpng16-16t64_1.6.48-1+deb13u5_arm64.deb 275192 11096ad43504ca24e8044bcc89e922d932a2812ff25568613b28d22235190ba5 https://deb.debian.org/debian/pool/main/libp/libpng1.6/libpng16-16t64_1.6.48-1+deb13u5_arm64.deb +libpulse0_17.0+dfsg1-2+b1_arm64.deb 236376 2de5610e3affe69cfb11110f890375261b47c9e0fc1a81eef7ecc4d6d2dc9a6d https://deb.debian.org/debian/pool/main/p/pulseaudio/libpulse0_17.0+dfsg1-2+b1_arm64.deb +libpython3.13_3.13.5-2+deb13u4_arm64.deb 1977636 d8f0a0d8d829988605ad66aff280c8c9770060b21868aae14a45e8ef872589ec https://deb.debian.org/debian/pool/main/p/python3.13/libpython3.13_3.13.5-2+deb13u4_arm64.deb +libsensors-config_1%3a3.6.2-2_all.deb 16236 3056da80c7d963af795dab480ab6f6f4b154ad4ac39f522dc52d17c834fea253 https://deb.debian.org/debian/pool/main/l/lm-sensors/libsensors-config_3.6.2-2_all.deb +libsensors5_1%3a3.6.2-2_arm64.deb 36444 7469f1ccce8afdba33d3b11847d0ecdf2221805a0694dee21ee4d622e8f72145 https://deb.debian.org/debian/pool/main/l/lm-sensors/libsensors5_3.6.2-2_arm64.deb +libshairplay0_0.9.0~git20180824.096b61a+dfsg1-3+b2_arm64.deb 100452 753f16da9a7953b3cec3e66ae5a379e72ed5e82ad7bb94443d13ec5f678c141d https://deb.debian.org/debian/pool/main/s/shairplay/libshairplay0_0.9.0~git20180824.096b61a+dfsg1-3+b2_arm64.deb +libsmbclient0_2%3a4.22.10+dfsg-0+deb13u2_arm64.deb 95392 db717af217d11808f02cdce5f4c12a4196a8dbff8d8713746e67265e5aff48c1 https://security.debian.org/debian-security/pool/updates/main/s/samba/libsmbclient0_4.22.10+dfsg-0+deb13u2_arm64.deb +libsndfile1_1.2.2-2+deb13u1_arm64.deb 185528 6c94f17e1f02778f98b96bb9ae19ff30085662e96685898a018c26bfd4bfc3f2 https://deb.debian.org/debian/pool/main/libs/libsndfile/libsndfile1_1.2.2-2+deb13u1_arm64.deb +libtag2_2.0.2-2_arm64.deb 310408 7fcf13ab2eb9f7671ea1692bb78c168e712e17f16cbd1e39e7c8dcc89dd077a7 https://deb.debian.org/debian/pool/main/t/taglib/libtag2_2.0.2-2_arm64.deb +libtalloc2_2%3a2.4.3+samba4.22.10+dfsg-0+deb13u2_arm64.deb 62316 20a23a60ef4030207d6f22e2f5bb20e57bb42a090fefb1a5606f23d34aba0510 https://security.debian.org/debian-security/pool/updates/main/s/samba/libtalloc2_2.4.3+samba4.22.10+dfsg-0+deb13u2_arm64.deb +libtdb1_2%3a1.4.13+samba4.22.10+dfsg-0+deb13u2_arm64.deb 82104 ba86a85fee628c394480248ea226cca10545b527b4ebf8799a1142c09265f327 https://security.debian.org/debian-security/pool/updates/main/s/samba/libtdb1_1.4.13+samba4.22.10+dfsg-0+deb13u2_arm64.deb +libtevent0t64_2%3a0.16.2+samba4.22.10+dfsg-0+deb13u2_arm64.deb 76128 2383d7e65a90114761f80c7ed801c516c3e09f74c78b92c6288a8be32ec2fce1 https://security.debian.org/debian-security/pool/updates/main/s/samba/libtevent0t64_0.16.2+samba4.22.10+dfsg-0+deb13u2_arm64.deb +libtinyxml2-11_11.0.0+dfsg-1+b1_arm64.deb 29808 596a10c41466c126c61ecfeed5d31f515066abccf5b2a2a6587e85473540eca9 https://deb.debian.org/debian/pool/main/t/tinyxml2/libtinyxml2-11_11.0.0+dfsg-1+b1_arm64.deb +libtinyxml2.6.2v5_2.6.2-7_arm64.deb 36500 6d6122c8011043f7f7bbd8f4e93c70f2dd191bd57a596cff61e2f6411c3f55ed https://deb.debian.org/debian/pool/main/t/tinyxml/libtinyxml2.6.2v5_2.6.2-7_arm64.deb +libudfread0_1.1.2-1+b2_arm64.deb 17312 badab108ac953987ced64c4b25a38c75ac391e6f4815cca4880619f313b4da22 https://deb.debian.org/debian/pool/main/libu/libudfread/libudfread0_1.1.2-1+b2_arm64.deb +libunibreak6_6.1-3_arm64.deb 21464 8689f3761d5ac0dd9119f187561dd61dbb8b273d3a684fc57fbcc3de9daac9ed https://deb.debian.org/debian/pool/main/libu/libunibreak/libunibreak6_6.1-3_arm64.deb +libvorbis0a_1.3.7-3_arm64.deb 83772 6e8e64ebe692dd2d019af7f914f2e3b81bf13d4b1ce2516fb4248a0310ef7cf6 https://deb.debian.org/debian/pool/main/libv/libvorbis/libvorbis0a_1.3.7-3_arm64.deb +libvorbisenc2_1.3.7-3_arm64.deb 75532 4a477b3ae19ba50a288f036c8176d61cd9f3836ba65472f88b95e3f6b3bf2dd3 https://deb.debian.org/debian/pool/main/libv/libvorbis/libvorbisenc2_1.3.7-3_arm64.deb +libvulkan1_1.4.309.0-1_arm64.deb 126808 f5f576d8b9e5703702a9f3254c8c5d130bef17da9db4aecd46ba86cec7a08930 https://deb.debian.org/debian/pool/main/v/vulkan-loader/libvulkan1_1.4.309.0-1_arm64.deb +libwacom-common_2.14.0-1_all.deb 102748 214956bd9b26600c4d5ac0d60c1685be6ef650644e36b712620c56fccd4d4f58 https://deb.debian.org/debian/pool/main/libw/libwacom/libwacom-common_2.14.0-1_all.deb +libwacom9_2.14.0-1_arm64.deb 24192 8fb8419c43fcc166f996c30cc262fcc86457bdd6f82656dad8619c88dc00b013 https://deb.debian.org/debian/pool/main/libw/libwacom/libwacom9_2.14.0-1_arm64.deb +libwayland-client0_1.23.1-3_arm64.deb 26140 8abdc649e46470548c545f827e16f52e1bc16d05d4a3794e6dedb2d74ce58c9a https://deb.debian.org/debian/pool/main/w/wayland/libwayland-client0_1.23.1-3_arm64.deb +libwayland-egl1_1.23.1-3_arm64.deb 5952 fe01fb2f3c5d5b54784563d9826fa505fab61dc30333d71343352bddd3812b18 https://deb.debian.org/debian/pool/main/w/wayland/libwayland-egl1_1.23.1-3_arm64.deb +libwayland-server0_1.23.1-3_arm64.deb 33656 062702d57cf07b42ab7f0fa1aac8bc3f605eb47f2f0b9ecb1614674140d79f1b https://deb.debian.org/debian/pool/main/w/wayland/libwayland-server0_1.23.1-3_arm64.deb +libwbclient0_2%3a4.22.10+dfsg-0+deb13u2_arm64.deb 70980 885cd7c331d6ffb0da72aeb76809deac82a8949ef415ed5c742a466d7fe1be96 https://security.debian.org/debian-security/pool/updates/main/s/samba/libwbclient0_4.22.10+dfsg-0+deb13u2_arm64.deb +libx11-6_2%3a1.8.12-1_arm64.deb 794952 646f2d3f2165c8eebceb7f4aaca31e97a818bb11a045964833a9f764b738bac2 https://deb.debian.org/debian/pool/main/libx/libx11/libx11-6_1.8.12-1_arm64.deb +libx11-data_2%3a1.8.12-1_all.deb 343072 c54f87069888f80ba4da586da6147d74c7598ccdd8b90906dbc4271fa414c738 https://deb.debian.org/debian/pool/main/libx/libx11/libx11-data_1.8.12-1_all.deb +libx11-xcb1_2%3a1.8.12-1_arm64.deb 247336 1eafaa4295e129d6a81e742ab8da43194daa6b916199923c8cef8ad358985894 https://deb.debian.org/debian/pool/main/libx/libx11/libx11-xcb1_1.8.12-1_arm64.deb +libxau6_1%3a1.0.11-1_arm64.deb 20576 ac1061728670f4626adaa1288953a0e6fb801c9cae72ee1c3231e63e2609d23a https://deb.debian.org/debian/pool/main/libx/libxau/libxau6_1.0.11-1_arm64.deb +libxcb-dri3-0_1.17.0-2+b1_arm64.deb 107068 247bf758dd78d6c285fdd47942ddacf4a3189fd50de4f0891c9bd9aea12da3a1 https://deb.debian.org/debian/pool/main/libx/libxcb/libxcb-dri3-0_1.17.0-2+b1_arm64.deb +libxcb-glx0_1.17.0-2+b1_arm64.deb 122608 a0d96ab5faea8fa1f3e095d6236597622a6b763f061f8f9fa23b77733608fdbc https://deb.debian.org/debian/pool/main/libx/libxcb/libxcb-glx0_1.17.0-2+b1_arm64.deb +libxcb-present0_1.17.0-2+b1_arm64.deb 105784 73521d715554a2230901d1d480465d6358b3083a5ce8801639595ffde6f31441 https://deb.debian.org/debian/pool/main/libx/libxcb/libxcb-present0_1.17.0-2+b1_arm64.deb +libxcb-randr0_1.17.0-2+b1_arm64.deb 116736 dbeb78d1d032959fe65548758ab12bfc6bb5919ec2984c3d871496b52aef929a https://deb.debian.org/debian/pool/main/libx/libxcb/libxcb-randr0_1.17.0-2+b1_arm64.deb +libxcb-shm0_1.17.0-2+b1_arm64.deb 105480 e9e871dc5d2f9265eb5d3028f0d0545cb6f8b748a68997b4c43f5a9416c7ead7 https://deb.debian.org/debian/pool/main/libx/libxcb/libxcb-shm0_1.17.0-2+b1_arm64.deb +libxcb-sync1_1.17.0-2+b1_arm64.deb 108792 eb428ac77daac9e5eea8a4e9ba48d1d78cc8ca400e415c8e4628cc729c6f4749 https://deb.debian.org/debian/pool/main/libx/libxcb/libxcb-sync1_1.17.0-2+b1_arm64.deb +libxcb-xfixes0_1.17.0-2+b1_arm64.deb 109644 19cdb8502827bc010993e69eaba09c670024976f7edd35b2b504bd646b141b0a https://deb.debian.org/debian/pool/main/libx/libxcb/libxcb-xfixes0_1.17.0-2+b1_arm64.deb +libxcb-xkb1_1.17.0-2+b1_arm64.deb 128572 739b023ad82b4c9ac2c6cc9e6cc8602a17a05f7f5387d45a5a60eafdc17bf0e5 https://deb.debian.org/debian/pool/main/libx/libxcb/libxcb-xkb1_1.17.0-2+b1_arm64.deb +libxcb1_1.17.0-2+b1_arm64.deb 143168 d0178198e80ed4cacdececabe2c112ec88c7a9258cc11a55b8e267ab14a90d82 https://deb.debian.org/debian/pool/main/libx/libxcb/libxcb1_1.17.0-2+b1_arm64.deb +libxdmcp6_1%3a1.1.5-1_arm64.deb 27752 e10bbb0802181992ecf091e9425171850eee16068729c109214ba4924f81fb52 https://deb.debian.org/debian/pool/main/libx/libxdmcp/libxdmcp6_1.1.5-1_arm64.deb +libxext6_2%3a1.3.4-1+b3_arm64.deb 49244 27cf208c6d2924b22ed3b9ceff304b992e1f07ef7b0ef00239585595581dac99 https://deb.debian.org/debian/pool/main/libx/libxext/libxext6_1.3.4-1+b3_arm64.deb +libxkbcommon-x11-0_1.7.0-2_arm64.deb 15168 414ec70596c8193b309279dc6d8a35f809ffe5047addc3587a6d52eb462bebc5 https://deb.debian.org/debian/pool/main/libx/libxkbcommon/libxkbcommon-x11-0_1.7.0-2_arm64.deb +libxkbcommon0_1.7.0-2_arm64.deb 106024 866888d3cfeb32388dd88a2615c4f78ebf8dd7b272821a83494cba260d8fa7d1 https://deb.debian.org/debian/pool/main/libx/libxkbcommon/libxkbcommon0_1.7.0-2_arm64.deb +libxml2_2.12.7+dfsg+really2.9.14-2.1+deb13u3_arm64.deb 631428 a66b9c960acef8ed9653223cbd00b155a92ebb758ea13a788c41609246c23bc7 https://deb.debian.org/debian/pool/main/libx/libxml2/libxml2_2.12.7+dfsg+really2.9.14-2.1+deb13u3_arm64.deb +libxshmfence1_1.3.3-1_arm64.deb 11104 34b494c83cebcf138d1f87c2cbdd5ee8efe9da2befbd69c2a025b0e214c64d2d https://deb.debian.org/debian/pool/main/libx/libxshmfence/libxshmfence1_1.3.3-1_arm64.deb +libxslt1.1_1.1.35-1.2+deb13u3_arm64.deb 222332 590a5d177dcfa2bf432f0d7db80b936ac7afadfebdb7f077d0c3305da20d1897 https://deb.debian.org/debian/pool/main/libx/libxslt/libxslt1.1_1.1.35-1.2+deb13u3_arm64.deb +libxxf86vm1_1%3a1.1.4-1+b4_arm64.deb 19176 9ed4bb25e311486207eeaa609decc3905aa9071c4c7f2a605829e3a79ec88b83 https://deb.debian.org/debian/pool/main/libx/libxxf86vm/libxxf86vm1_1.1.4-1+b4_arm64.deb +libz3-4_4.13.3-1_arm64.deb 7506868 29a7b924a5a38c9ac4e2a64aa11586a9c54f26b579b18e4f75adf8f7c1106e45 https://deb.debian.org/debian/pool/main/z/z3/libz3-4_4.13.3-1_arm64.deb +mariadb-common_1%3a11.8.6-0+deb13u1_all.deb 29544 b5116ff56a60d471332d45bfd4ec50391fdcf5a40747bf7fe23ec8551cb71f9c https://deb.debian.org/debian/pool/main/m/mariadb/mariadb-common_11.8.6-0+deb13u1_all.deb +mesa-libgallium_25.0.7-2+deb13u1_arm64.deb 8032536 a27576030704ff305adaef8bd143954b281680861d5bfe21aabd7d376a939149 https://deb.debian.org/debian/pool/main/m/mesa/mesa-libgallium_25.0.7-2+deb13u1_arm64.deb +mesa-utils-bin_9.0.0-2+b2_arm64.deb 192896 e7906035da53770d63ff477f626c0c7209a799caf781714b229263a3d75ec03c https://deb.debian.org/debian/pool/main/m/mesa-demos/mesa-utils-bin_9.0.0-2+b2_arm64.deb +mesa-utils_9.0.0-2+b2_arm64.deb 955092 aaa2276c9e679c811fb980c98e20acbd5ea93403a8d92792315f05f82a2e2bee https://deb.debian.org/debian/pool/main/m/mesa-demos/mesa-utils_9.0.0-2+b2_arm64.deb +mesa-vulkan-drivers_25.0.7-2+deb13u1_arm64.deb 11373712 b73e1b64f26d55c981339533f3eb64397fda4a8e86dd5593ed15e1c48711141d https://deb.debian.org/debian/pool/main/m/mesa/mesa-vulkan-drivers_25.0.7-2+deb13u1_arm64.deb +mysql-common_5.8+1.1.1_all.deb 6784 a4e36be39be45d6f5267ac71b92d7eecd40a15f25d52a0bfb9c5eaca1b90e534 https://deb.debian.org/debian/pool/main/m/mysql-defaults/mysql-common_5.8+1.1.1_all.deb +nfs-common_1%3a2.8.3-1_arm64.deb 260196 1c45d155086ecef6f531122a6d9a2dbf873f85a211eebf0fdf2841c3ee2f93d0 https://deb.debian.org/debian/pool/main/n/nfs-utils/nfs-common_2.8.3-1_arm64.deb +pci.ids_0.0~2025.06.09-1_all.deb 269172 132bc7520473e53939e277cbf4512261b8d73ae244d77c277f674ecaf174598c https://deb.debian.org/debian/pool/main/p/pci.ids/pci.ids_0.0~2025.06.09-1_all.deb +pciutils_1%3a3.13.0-2_arm64.deb 122800 e60b3a48ce93ed5634713226b6ca623a7e7a77007ac0de0a38698c84f36e1867 https://deb.debian.org/debian/pool/main/p/pciutils/pciutils_3.13.0-2_arm64.deb +python-apt-common_3.0.0_all.deb 68248 9aacb635fa04e01ab56a0af4869a034a94aadcd564d662a91048cd19fe7d6e0c https://deb.debian.org/debian/pool/main/p/python-apt/python-apt-common_3.0.0_all.deb +python3-apt_3.0.0_arm64.deb 157212 7f9c928895e7141ecea9d53ee93962cfb37d48a2015abc99d95a22e77535f8a7 https://deb.debian.org/debian/pool/main/p/python-apt/python3-apt_3.0.0_arm64.deb +python3-dbus_1.4.0-1_arm64.deb 96064 fe98d700adc6239a01ffa7441bb554a78b43bf7be4edf026a8be279266dde919 https://deb.debian.org/debian/pool/main/d/dbus-python/python3-dbus_1.4.0-1_arm64.deb +python3-distro-info_1.13_all.deb 7736 11848888a90676314dd977fd2788e2d1f11f5e471d879313b8158a28488dc062 https://deb.debian.org/debian/pool/main/d/distro-info/python3-distro-info_1.13_all.deb +rpcbind_1.2.7-1_arm64.deb 47016 1ff91ee1a472c5b3891653a2622d51c9bbb0d11f42e0bf62614d88c16e9d1003 https://deb.debian.org/debian/pool/main/r/rpcbind/rpcbind_1.2.7-1_arm64.deb +samba-libs_2%3a4.22.10+dfsg-0+deb13u2_arm64.deb 5646024 23dd06b3d384ff0ad2da844a369c45962eb2663ac66e7078edf01c811936cd16 https://security.debian.org/debian-security/pool/updates/main/s/samba/samba-libs_4.22.10+dfsg-0+deb13u2_arm64.deb +unattended-upgrades_2.12_all.deb 66912 a5e37d196598d2f2a7cb8c3ded760b77283d0ecf110d2f90d77d4821e3512eb6 https://deb.debian.org/debian/pool/main/u/unattended-upgrades/unattended-upgrades_2.12_all.deb +vulkan-tools_1.4.304.0+dfsg1-1_arm64.deb 261792 de3b21a2a8a2b2cf520cad433878589c70366050f52570a4fe37556b7c748647 https://deb.debian.org/debian/pool/main/v/vulkan-tools/vulkan-tools_1.4.304.0+dfsg1-1_arm64.deb diff --git a/packages/fetch-locked-debs.sh b/packages/fetch-locked-debs.sh new file mode 100755 index 0000000..b04ab56 --- /dev/null +++ b/packages/fetch-locked-debs.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Fetch the exact Debian package payload described by the integrity lock. + +set -Eeuo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) +PROJECT_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd -P) +LOCK_FILE=${1:-"$SCRIPT_DIR/debian-packages.lock.tsv"} +OUTPUT_DIR=${2:-"$PROJECT_DIR/work/apt/archives"} + +[[ -s "$LOCK_FILE" ]] || { echo "Missing package lock: $LOCK_FILE" >&2; exit 1; } +mkdir -p "$OUTPUT_DIR" + +while IFS=$'\t' read -r filename expected_size expected_sha url; do + [[ "$filename" == '# filename' ]] && continue + [[ -n "$filename" && "$expected_sha" =~ ^[0-9a-f]{64}$ ]] || { + echo "Malformed package lock row: $filename" >&2 + exit 1 + } + output="$OUTPUT_DIR/$filename" + if [[ -f "$output" ]] && + [[ "$(stat -c '%s' "$output")" == "$expected_size" ]] && + [[ "$(sha256sum "$output" | awk '{print $1}')" == "$expected_sha" ]]; then + continue + fi + partial="$output.partial" + rm -f -- "$partial" + curl --fail --location --show-error --output "$partial" "$url" + [[ "$(stat -c '%s' "$partial")" == "$expected_size" ]] || { + echo "Size mismatch for $filename" >&2 + exit 1 + } + echo "$expected_sha $partial" | sha256sum --check --status || { + echo "Checksum mismatch for $filename" >&2 + exit 1 + } + mv -- "$partial" "$output" +done <"$LOCK_FILE" + +echo "Verified locked package bundle: $OUTPUT_DIR" diff --git a/packages/generate-lock.sh b/packages/generate-lock.sh new file mode 100755 index 0000000..0bce5d1 --- /dev/null +++ b/packages/generate-lock.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# Generate an integrity lock for an already-resolved Debian package bundle. + +set -Eeuo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) +PROJECT_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd -P) +ARCHIVE_DIR=${1:-"$PROJECT_DIR/work/apt/archives"} +LIST_DIR=${2:-"$PROJECT_DIR/work/apt/lists"} +OUTPUT=${3:-"$SCRIPT_DIR/debian-packages.lock.tsv"} + +[[ -d "$ARCHIVE_DIR" && -d "$LIST_DIR" ]] || { + echo "Usage: $0 [ARCHIVE-DIR LIST-DIR OUTPUT]" >&2 + exit 2 +} + +INDEX=$(mktemp) +OUTPUT_TMP=$(mktemp) +trap 'rm -f -- "$INDEX" "$OUTPUT_TMP"' EXIT + +for package_list in "$LIST_DIR"/*_Packages; do + [[ -f "$package_list" ]] || continue + case "$(basename -- "$package_list")" in + security.debian.org_*) base_url=https://security.debian.org/debian-security ;; + deb.debian.org_*) base_url=https://deb.debian.org/debian ;; + *) continue ;; + esac + awk -v base_url="$base_url" ' + /^Filename: / { filename=substr($0, 11) } + /^Size: / { size=substr($0, 7) } + /^SHA256: / { sha=substr($0, 9) } + /^$/ { + if (filename != "" && size != "" && sha != "") { + print sha "\t" size "\t" base_url "/" filename + } + filename=size=sha="" + } + END { + if (filename != "" && size != "" && sha != "") { + print sha "\t" size "\t" base_url "/" filename + } + } + ' "$package_list" >>"$INDEX" +done +LC_ALL=C sort -u -o "$INDEX" "$INDEX" + +printf '# filename\tsize\tsha256\turl\n' >"$OUTPUT_TMP" +while IFS= read -r -d '' package_path; do + filename=$(basename -- "$package_path") + size=$(stat -c '%s' "$package_path") + sha=$(sha256sum "$package_path" | awk '{print $1}') + match=$(awk -F '\t' -v sha="$sha" -v size="$size" \ + '$1 == sha && $2 == size { print; exit }' "$INDEX") + [[ -n "$match" ]] || { + echo "No signed package-index record for $filename" >&2 + exit 1 + } + url=${match#*$'\t'} + url=${url#*$'\t'} + printf '%s\t%s\t%s\t%s\n' "$filename" "$size" "$sha" "$url" \ + >>"$OUTPUT_TMP" +done < <(find "$ARCHIVE_DIR" -maxdepth 1 -type f -name '*.deb' -print0 | sort -z) + +install -m 0644 "$OUTPUT_TMP" "$OUTPUT" +echo "Locked $(($(wc -l <"$OUTPUT") - 1)) Debian packages in $OUTPUT" diff --git a/runtime/README.md b/runtime/README.md new file mode 100644 index 0000000..4156819 --- /dev/null +++ b/runtime/README.md @@ -0,0 +1,100 @@ +# RK1 runtime and hardware diagnostics + +This directory installs the local RKNN C runtime and supplies the two commands +used to qualify an RK3588 media image. It does not install RKNN Toolkit's model +conversion environment, Python wheels, or `rknn_server`. + +## Pinned RKNN runtime + +`rknn-version.env` locks RKNN Toolkit2 v2.3.2 to commit +`42aa1d426c0a9e0869b6374edba009f7208a1926`. The installer verifies the SHA-256 +of the ARM64 runtime, C header, RK3588 MobileNet model, demo image, and license +before installing anything. A tag move or damaged download therefore fails the +image build. + +Run the installer inside the target chroot: + +```sh +runtime/install-rknn-runtime.sh +``` + +Or install into a mounted root filesystem from an ARM64 build host: + +```sh +runtime/install-rknn-runtime.sh --rootfs /path/to/rootfs +``` + +For an offline/reproducible build, provide the pinned checkout explicitly: + +```sh +runtime/install-rknn-runtime.sh \ + --rootfs /path/to/rootfs \ + --source-dir /path/to/rknn-toolkit2-v2.3.2 +``` + +The build host needs Bash, Git when downloading, an ARM64 C compiler, +`coreutils`, and standard install utilities. Native compilation on the RK1 is +the supported default; a cross-build can select a compiler with `--cc`. + +Files are installed below `/opt/rknn/2.3.2`, with `/opt/rknn/current` as the +stable link. The runtime path is registered in `/etc/ld.so.conf.d/rknn.conf`, +and the inference test also embeds that path as an ELF rpath. The test uses a +deterministic synthetic tensor and verifies model initialization, selected-core +submission, and finite output. It is a hardware/runtime smoke test, not a +MobileNet accuracy benchmark. + +## Hardware self-test + +The installed command inventories HDMI/EDID, Panthor or Mali, DRM render +nodes, RKMPP decoder and encoder bindings, AV1, RGA2/RGA3, RKNN, HDMI ALSA, +Ethernet link state, eMMC, NVMe, and the root filesystem: + +```sh +rk1-media-selftest +rk1-media-selftest --quick +rk1-media-selftest --json +``` + +The normal run adds short Vulkan, H.264/HEVC/MJPEG encode, and NPU core +0/1/2/combined workloads when their tools are installed. `--quick` performs no +active workloads. To test 4K decoding and RKMPP-to-RGA zero-copy scaling, place +licensed samples in a directory with names containing `h264`, `hevc`, `vp9`, +and `av1`, then run: + +```sh +rk1-media-selftest --media-dir /usr/share/rk1-media/samples +``` + +Missing tools and tests that cannot apply are `SKIP`; absent or disconnected +hardware is normally `WARN`; an advertised capability whose active workload +fails is `FAIL`. The exit status is zero unless a check fails. `--strict` also +makes warnings return status 1. Invocation errors return status 2. + +## Diagnostic bundle + +Create a local archive suitable for troubleshooting a blank display or missing +accelerator: + +```sh +sudo rk1-media-diagnostics +sudo rk1-media-diagnostics --include-active-tests \ + --media-dir /usr/share/rk1-media/samples +``` + +The collector records the JSON quick test, DRM connectors, decoded EDID, +drivers, filtered kernel messages, relevant packages, ALSA devices, link state, +and storage topology. It does not upload anything. It omits raw EDID, disk +serials and UUIDs, addresses, user files, SSH material, and unfiltered journals, +and applies basic redaction; review the bundle before sharing it. + +## Tests + +```sh +runtime/tests/run-tests.sh +RKNN_SOURCE_DIR=/path/to/rknn-toolkit2-v2.3.2 \ + runtime/tests/run-tests.sh +``` + +The first form tests syntax, absent-hardware handling, JSON, synthetic sysfs, +and diagnostic collection. Supplying `RKNN_SOURCE_DIR` also compiles and stages +the pinned runtime into a temporary root filesystem. diff --git a/runtime/install-rknn-runtime.sh b/runtime/install-rknn-runtime.sh new file mode 100755 index 0000000..e6f8bb4 --- /dev/null +++ b/runtime/install-rknn-runtime.sh @@ -0,0 +1,209 @@ +#!/usr/bin/env bash +# Install the pinned RKNN C runtime, test model, and RK1 diagnostic tools. + +set -Eeuo pipefail +umask 022 + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) +# shellcheck source=rknn-version.env +source "$SCRIPT_DIR/rknn-version.env" + +ROOTFS="/" +SOURCE_DIR="" +CC_BIN=${CC:-cc} +KEEP_WORK=0 +WORK_DIR="" + +usage() { + cat <<'EOF' +Usage: install-rknn-runtime.sh [OPTIONS] + +Options: + --rootfs PATH Install below PATH (default: /). + --source-dir PATH Use an existing rknn-toolkit2 v2.3.2 checkout. + --cc COMMAND C compiler for the target (default: $CC or cc). + --keep-work Preserve a downloaded checkout for inspection. + -h, --help Show this help. + +Without --source-dir, the script fetches only the required paths from the +immutable upstream commit. Network access and git are then required. +EOF +} + +die() { + printf 'install-rknn-runtime: error: %s\n' "$*" >&2 + exit 1 +} + +note() { + printf 'install-rknn-runtime: %s\n' "$*" >&2 +} + +while (($#)); do + case "$1" in + --rootfs) + (($# >= 2)) || die "--rootfs requires a path" + ROOTFS=$2 + shift 2 + ;; + --source-dir) + (($# >= 2)) || die "--source-dir requires a path" + SOURCE_DIR=$2 + shift 2 + ;; + --cc) + (($# >= 2)) || die "--cc requires a compiler command" + CC_BIN=$2 + shift 2 + ;; + --keep-work) + KEEP_WORK=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "unknown option: $1" + ;; + esac +done + +[[ -d "$ROOTFS" ]] || die "rootfs is not a directory: $ROOTFS" +ROOTFS=$(readlink -f -- "$ROOTFS") +[[ -n "$ROOTFS" ]] || die "could not resolve rootfs" + +if [[ "$ROOTFS" == "/" && ${EUID:-$(id -u)} -ne 0 ]]; then + die "installing into / requires root" +fi + +for tool in install ln readlink sha256sum "$CC_BIN"; do + command -v "$tool" >/dev/null 2>&1 || die "required command not found: $tool" +done + +cleanup() { + if [[ -n "$WORK_DIR" && -d "$WORK_DIR" && $KEEP_WORK -eq 0 ]]; then + rm -rf -- "$WORK_DIR" + elif [[ -n "$WORK_DIR" && -d "$WORK_DIR" ]]; then + note "preserved work directory: $WORK_DIR" + fi +} +trap cleanup EXIT + +verify_sha256() { + local expected=$1 + local path=$2 + local actual + [[ -f "$path" ]] || die "required upstream asset is missing: $path" + actual=$(sha256sum -- "$path") + actual=${actual%% *} + [[ "$actual" == "$expected" ]] || + die "checksum mismatch for $path (expected $expected, got $actual)" +} + +if [[ -z "$SOURCE_DIR" ]]; then + command -v git >/dev/null 2>&1 || die "git is required without --source-dir" + WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/rknn-v232.XXXXXXXX") + SOURCE_DIR="$WORK_DIR/source" + note "fetching RKNN Toolkit2 commit $RKNN_COMMIT" + git init -q "$SOURCE_DIR" + git -C "$SOURCE_DIR" remote add origin "$RKNN_REPOSITORY" + git -C "$SOURCE_DIR" config remote.origin.promisor true + git -C "$SOURCE_DIR" config remote.origin.partialclonefilter blob:none + git -C "$SOURCE_DIR" sparse-checkout init --cone + git -C "$SOURCE_DIR" sparse-checkout set \ + rknpu2/runtime/Linux/librknn_api \ + rknpu2/examples/rknn_api_demo/model + git -C "$SOURCE_DIR" fetch -q --depth 1 --filter=blob:none origin \ + "refs/tags/$RKNN_TAG" + git -C "$SOURCE_DIR" checkout -q --detach FETCH_HEAD +fi + +SOURCE_DIR=$(readlink -f -- "$SOURCE_DIR") +[[ -d "$SOURCE_DIR" ]] || die "source directory is not a directory" +if [[ -d "$SOURCE_DIR/.git" ]]; then + source_commit=$(git -C "$SOURCE_DIR" rev-parse HEAD) + [[ "$source_commit" == "$RKNN_COMMIT" ]] || + die "source checkout is $source_commit, expected $RKNN_COMMIT" +fi + +HEADER="$SOURCE_DIR/rknpu2/runtime/Linux/librknn_api/include/rknn_api.h" +RUNTIME_SO="$SOURCE_DIR/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so" +MODEL="$SOURCE_DIR/rknpu2/examples/rknn_api_demo/model/RK3588/mobilenet_v1.rknn" +DEMO_IMAGE="$SOURCE_DIR/rknpu2/examples/rknn_api_demo/model/dog_224x224.jpg" +LICENSE_FILE="$SOURCE_DIR/LICENSE" + +verify_sha256 "$RKNN_HEADER_SHA256" "$HEADER" +verify_sha256 "$RKNN_RUNTIME_AARCH64_SHA256" "$RUNTIME_SO" +verify_sha256 "$RKNN_MOBILENET_RK3588_SHA256" "$MODEL" +verify_sha256 "$RKNN_DEMO_IMAGE_SHA256" "$DEMO_IMAGE" +verify_sha256 "$RKNN_LICENSE_SHA256" "$LICENSE_FILE" + +if [[ -z "$WORK_DIR" ]]; then + WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/rknn-v232-build.XXXXXXXX") +fi +TEST_BINARY="$WORK_DIR/rknn-inference-test" + +note "building the ARM64 RKNN smoke test with $CC_BIN" +"$CC_BIN" \ + -std=c11 -D_POSIX_C_SOURCE=200809L -O2 -Wall -Wextra -Wpedantic \ + -fstack-protector-strong -D_FORTIFY_SOURCE=2 \ + -I"$(dirname -- "$HEADER")" \ + "$SCRIPT_DIR/rknn-inference-test.c" \ + -L"$(dirname -- "$RUNTIME_SO")" -Wl,--as-needed \ + -Wl,-z,relro,-z,now -Wl,-rpath,/opt/rknn/current/lib \ + -lrknnrt -lm -o "$TEST_BINARY" + +VERSION_ROOT="$ROOTFS/opt/rknn/$RKNN_VERSION" +install -d -m 0755 \ + "$VERSION_ROOT/bin" \ + "$VERSION_ROOT/include" \ + "$VERSION_ROOT/lib" \ + "$VERSION_ROOT/share/models/rk3588" \ + "$VERSION_ROOT/share/demo" \ + "$VERSION_ROOT/share/licenses" \ + "$ROOTFS/etc/ld.so.conf.d" \ + "$ROOTFS/usr/local/bin" \ + "$ROOTFS/usr/local/sbin" + +install -m 0755 "$TEST_BINARY" "$VERSION_ROOT/bin/rknn-inference-test" +install -m 0644 "$HEADER" "$VERSION_ROOT/include/rknn_api.h" +install -m 0644 "$RUNTIME_SO" "$VERSION_ROOT/lib/librknnrt.so" +ln -sfn librknnrt.so "$VERSION_ROOT/lib/librknn_api.so" +install -m 0644 "$MODEL" \ + "$VERSION_ROOT/share/models/rk3588/mobilenet_v1.rknn" +install -m 0644 "$DEMO_IMAGE" "$VERSION_ROOT/share/demo/dog_224x224.jpg" +install -m 0644 "$LICENSE_FILE" \ + "$VERSION_ROOT/share/licenses/rknn-toolkit2-LICENSE" + +cat >"$VERSION_ROOT/manifest.env" <"$ROOTFS/etc/ld.so.conf.d/rknn.conf" +chmod 0644 "$ROOTFS/etc/ld.so.conf.d/rknn.conf" + +install -m 0755 "$SCRIPT_DIR/rk1-media-selftest" \ + "$ROOTFS/usr/local/bin/rk1-media-selftest" +install -m 0755 "$SCRIPT_DIR/rk1-media-diagnostics" \ + "$ROOTFS/usr/local/sbin/rk1-media-diagnostics" + +if command -v ldconfig >/dev/null 2>&1 && + [[ -e "$ROOTFS/etc/ld.so.conf" && -d "$ROOTFS/lib" ]]; then + if ! ldconfig -r "$ROOTFS"; then + note "warning: ldconfig failed; the smoke test still has an embedded rpath" + fi +fi + +note "installed RKNN Runtime $RKNN_VERSION below $ROOTFS/opt/rknn/current" diff --git a/runtime/make-offline-bundle.sh b/runtime/make-offline-bundle.sh new file mode 100755 index 0000000..c8e858a --- /dev/null +++ b/runtime/make-offline-bundle.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Extract only the immutable RKNN assets needed by install-rknn-runtime.sh. + +set -Eeuo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) +# shellcheck source=rknn-version.env +source "$SCRIPT_DIR/rknn-version.env" + +[[ $# -eq 2 ]] || { + echo "Usage: $0 PINNED-RKNN-CHECKOUT OUTPUT-DIRECTORY" >&2 + exit 2 +} + +SOURCE_DIR=$(readlink -f -- "$1") +OUTPUT_DIR=$2 +[[ -d "$SOURCE_DIR/.git" ]] || { + echo "Source must be a Git checkout: $SOURCE_DIR" >&2 + exit 1 +} +[[ "$(git -C "$SOURCE_DIR" rev-parse HEAD)" == "$RKNN_COMMIT" ]] || { + echo "RKNN checkout is not pinned commit $RKNN_COMMIT" >&2 + exit 1 +} +[[ ! -e "$OUTPUT_DIR" ]] || { + echo "Refusing to replace existing output: $OUTPUT_DIR" >&2 + exit 1 +} + +declare -a assets=( + "rknpu2/runtime/Linux/librknn_api/include/rknn_api.h:$RKNN_HEADER_SHA256" + "rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so:$RKNN_RUNTIME_AARCH64_SHA256" + "rknpu2/examples/rknn_api_demo/model/RK3588/mobilenet_v1.rknn:$RKNN_MOBILENET_RK3588_SHA256" + "rknpu2/examples/rknn_api_demo/model/dog_224x224.jpg:$RKNN_DEMO_IMAGE_SHA256" + "LICENSE:$RKNN_LICENSE_SHA256" +) + +for entry in "${assets[@]}"; do + relative_path=${entry%%:*} + expected_sha=${entry##*:} + actual_sha=$(sha256sum "$SOURCE_DIR/$relative_path" | awk '{print $1}') + [[ "$actual_sha" == "$expected_sha" ]] || { + echo "Checksum mismatch: $relative_path" >&2 + exit 1 + } + install -D -m 0644 "$SOURCE_DIR/$relative_path" "$OUTPUT_DIR/$relative_path" +done + +cat >"$OUTPUT_DIR/SOURCE.env" <&2 + exit 2 +} + +while (($#)); do + case "$1" in + --output) + (($# >= 2)) || die "--output requires a path" + OUTPUT=$2 + shift 2 + ;; + --directory) + DIRECTORY_OUTPUT=1 + shift + ;; + --include-active-tests) + ACTIVE_TESTS=1 + shift + ;; + --media-dir) + (($# >= 2)) || die "--media-dir requires a path" + MEDIA_DIR=$2 + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "unknown option: $1" + ;; + esac +done + +if [[ -n "$MEDIA_DIR" && ! -d "$MEDIA_DIR" ]]; then + die "media directory is not a directory: $MEDIA_DIR" +fi + +safe_host=$(hostname 2>/dev/null || printf rk1) +safe_host=${safe_host//[^A-Za-z0-9._-]/_} +timestamp=$(date -u +%Y%m%dT%H%M%SZ) +if [[ -z "$OUTPUT" ]]; then + if ((DIRECTORY_OUTPUT)); then + OUTPUT="$PWD/rk1-media-diagnostics-${safe_host}-${timestamp}" + else + OUTPUT="$PWD/rk1-media-diagnostics-${safe_host}-${timestamp}.tar.gz" + fi +fi + +output_parent=$(dirname -- "$OUTPUT") +[[ -d "$output_parent" ]] || die "output parent does not exist: $output_parent" +output_parent=$(cd -- "$output_parent" && pwd -P) +OUTPUT="$output_parent/$(basename -- "$OUTPUT")" +[[ ! -e "$OUTPUT" && ! -L "$OUTPUT" ]] || die "refusing to overwrite $OUTPUT" + +TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/rk1-media-diagnostics.XXXXXXXX") +REPORT="$TEMP_DIR/report" +mkdir -p "$REPORT" + +cleanup() { + if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then + rm -rf -- "$TEMP_DIR" + fi +} +trap cleanup EXIT + +sanitize_stream() { + # Redact common MAC, IPv4, root-device, and static-IP command-line forms. + sed -E \ + -e 's/([[:xdigit:]]{2}:){5}[[:xdigit:]]{2}//g' \ + -e 's/([[:space:]=]|^)([0-9]{1,3}\.){3}[0-9]{1,3}([[:space:]\/:]|$)/\1\3/g' \ + -e 's/(root=)[^[:space:]]+/\1/g' \ + -e 's/(ip=)[^[:space:]]+/\1/g' \ + -e 's/(UUID|PARTUUID)=[A-Za-z0-9-]+/\1=/g' \ + -e 's/([Ss]erial([ _-]?[Nn]umber)?[=:][[:space:]]*)[^[:space:],;]+/\1/g' +} + +run_capture() { + local destination=$1 + shift + { + printf '$' + printf ' %q' "$@" + printf '\n' + if command -v "$1" >/dev/null 2>&1; then + timeout 45s "$@" 2>&1 || + printf '[command exited %d]\n' "$?" + else + printf '[command unavailable: %s]\n' "$1" + fi + } | sanitize_stream >"$REPORT/$destination" +} + +append_command() { + local destination=$1 + shift + { + printf '\n$' + printf ' %q' "$@" + printf '\n' + if command -v "$1" >/dev/null 2>&1; then + timeout 45s "$@" 2>&1 || + printf '[command exited %d]\n' "$?" + else + printf '[command unavailable: %s]\n' "$1" + fi + } | sanitize_stream >>"$REPORT/$destination" +} + +cat >"$REPORT/README.txt" <<'EOF' +RK1 media diagnostic bundle + +This is a read-only snapshot for diagnosing HDMI, GPU, RKMPP VPU, RGA, +RKNPU, ALSA, Ethernet, and storage enumeration. Commands that were missing or +permission-denied are recorded rather than treated as collector failures. + +The collector attempts to redact IP and MAC addresses and omits raw EDID, +disk serials/UUIDs, environment variables, home directories, SSH material, +and unfiltered journals. Review every file before sharing the bundle. +EOF + +{ + printf 'collected_utc=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" + printf 'collector_version=1\n' + printf 'active_tests=%s\n' "$ACTIVE_TESTS" + printf 'kernel=' + uname -srvm 2>/dev/null || true + printf 'architecture=' + uname -m 2>/dev/null || true + if [[ -r /etc/os-release ]]; then + printf '\n[os-release]\n' + grep -E '^(NAME|VERSION|VERSION_ID|ID|ID_LIKE)=' /etc/os-release || true + fi + if [[ -r "$PROC_ROOT/device-tree/model" ]]; then + printf '\nboard_model=' + tr -d '\000' <"$PROC_ROOT/device-tree/model" || true + printf '\n' + fi + if [[ -r "$PROC_ROOT/cmdline" ]]; then + printf '\n[kernel-command-line-redacted]\n' + sanitize_stream <"$PROC_ROOT/cmdline" + printf '\n' + fi +} >"$REPORT/system.txt" + +SELFTEST="" +if command -v rk1-media-selftest >/dev/null 2>&1; then + SELFTEST=$(command -v rk1-media-selftest) +elif [[ -x "$(dirname -- "${BASH_SOURCE[0]}")/rk1-media-selftest" ]]; then + SELFTEST="$(dirname -- "${BASH_SOURCE[0]}")/rk1-media-selftest" +fi + +if [[ -n "$SELFTEST" ]]; then + selftest_args=(--json) + if ((ACTIVE_TESTS == 0)); then + selftest_args+=(--quick) + fi + if [[ -n "$MEDIA_DIR" ]]; then + selftest_args+=(--media-dir "$MEDIA_DIR") + fi + if ! timeout 600s "$SELFTEST" "${selftest_args[@]}" \ + >"$REPORT/selftest.json" 2>"$REPORT/selftest.stderr"; then + printf 'self-test returned a nonzero status; inspect its JSON and stderr\n' \ + >"$REPORT/selftest-status.txt" + fi +else + printf '{"error":"rk1-media-selftest is unavailable"}\n' \ + >"$REPORT/selftest.json" +fi + +# DRM connector details. EDID is represented by size, hash, and decoded text, +# never by the raw binary blob. +{ + shopt -s nullglob + connectors=("$SYS_ROOT"/class/drm/card*-*) + shopt -u nullglob + if ((${#connectors[@]} == 0)); then + printf 'No DRM connectors found.\n' + fi + for connector in "${connectors[@]}"; do + [[ -d "$connector" ]] || continue + printf '\n[%s]\n' "${connector##*/}" + for attribute in status enabled dpms link_status; do + if [[ -r "$connector/$attribute" ]]; then + printf '%s=' "$attribute" + tr -d '\000' <"$connector/$attribute" 2>/dev/null || true + printf '\n' + fi + done + if [[ -r "$connector/modes" ]]; then + printf 'modes:\n' + sed 's/^/ /' "$connector/modes" 2>/dev/null || true + fi + if [[ -s "$connector/edid" ]]; then + printf 'edid_size=%s\n' "$(wc -c <"$connector/edid")" + printf 'edid_sha256=%s\n' \ + "$(sha256sum "$connector/edid" | awk '{print $1}')" + if command -v edid-decode >/dev/null 2>&1; then + printf 'decoded_edid:\n' + timeout 15s edid-decode "$connector/edid" 2>&1 | + sed 's/^/ /' || true + fi + fi + done +} | sanitize_stream >"$REPORT/drm-connectors.txt" + +run_capture drm-modetest.txt modetest -c -p +run_capture kernel-modules.txt lsmod + +{ + printf '[platform GPU driver bindings]\n' + for driver in panthor panfrost mali; do + directory="$SYS_ROOT/bus/platform/drivers/$driver" + [[ -d "$directory" ]] || continue + printf '%s:\n' "$driver" + find "$directory" -mindepth 1 -maxdepth 1 \( -type l -o -type d \) \ + -printf ' %f\n' 2>/dev/null | sort + done + printf '\n[GPU devfreq]\n' + shopt -s nullglob + gpu_nodes=("$SYS_ROOT"/class/devfreq/*.gpu) + shopt -u nullglob + for node in "${gpu_nodes[@]}"; do + printf '%s\n' "${node##*/}" + for attribute in cur_freq min_freq max_freq governor available_frequencies; do + if [[ -r "$node/$attribute" ]]; then + printf ' %s=' "$attribute" + tr -d '\000' <"$node/$attribute" 2>/dev/null || true + printf '\n' + fi + done + done +} >"$REPORT/gpu-sysfs.txt" +run_capture vulkan.txt vulkaninfo --summary +append_command vulkan.txt eglinfo -B + +{ + printf '[media and accelerator platform bindings]\n' + for pattern in 'mpp*' 'rga*' 'rockchip-rga' 'RKNPU' 'rknpu'; do + shopt -s nullglob + directories=("$SYS_ROOT"/bus/platform/drivers/$pattern) + shopt -u nullglob + for directory in "${directories[@]}"; do + [[ -d "$directory" ]] || continue + printf '\n%s:\n' "${directory##*/}" + find "$directory" -mindepth 1 -maxdepth 1 \( -type l -o -type d \) \ + -printf ' %f\n' 2>/dev/null | sort + done + done + printf '\n[device nodes]\n' + shopt -s nullglob + nodes=("$DEV_ROOT"/dri/* "$DEV_ROOT"/mpp_service \ + "$DEV_ROOT"/rga "$DEV_ROOT"/rknpu* "$DEV_ROOT"/mali*) + shopt -u nullglob + for node in "${nodes[@]}"; do + stat -c '%A %U:%G %t:%T %n' "$node" 2>/dev/null || true + done +} >"$REPORT/accelerators.txt" + +FFMPEG="" +for candidate in /opt/rkmedia/bin/ffmpeg-rk /usr/local/bin/ffmpeg-rk; do + if [[ -x "$candidate" ]]; then + FFMPEG=$candidate + break + fi +done +if [[ -z "$FFMPEG" ]] && command -v ffmpeg-rk >/dev/null 2>&1; then + FFMPEG=$(command -v ffmpeg-rk) +fi +if [[ -n "$FFMPEG" ]]; then + run_capture ffmpeg-rk.txt "$FFMPEG" -hide_banner -version + append_command ffmpeg-rk.txt "$FFMPEG" -hide_banner -hwaccels + { + printf '\n[Rockchip encoders, decoders, and filters]\n' + "$FFMPEG" -hide_banner -decoders 2>/dev/null | + grep -Ei 'h264|hevc|vp9|av1|rkmpp' || true + "$FFMPEG" -hide_banner -encoders 2>/dev/null | + grep -Ei 'h264|hevc|mjpeg|rkmpp' || true + "$FFMPEG" -hide_banner -filters 2>/dev/null | + grep -Ei 'rkrga|drm' || true + } >>"$REPORT/ffmpeg-rk.txt" +else + printf 'ffmpeg-rk is unavailable\n' >"$REPORT/ffmpeg-rk.txt" +fi + +{ + printf '[RKNN installation]\n' + if [[ -r "$RKNN_HOME/manifest.env" ]]; then + sed -E 's#(REPOSITORY=).*#\1#' "$RKNN_HOME/manifest.env" + else + printf 'manifest unavailable at %s\n' "$RKNN_HOME/manifest.env" + fi + for asset in \ + "$RKNN_HOME/lib/librknnrt.so" \ + "$RKNN_HOME/share/models/rk3588/mobilenet_v1.rknn" \ + "$RKNN_HOME/bin/rknn-inference-test"; do + if [[ -r "$asset" ]]; then + sha256sum "$asset" + else + printf 'missing: %s\n' "$asset" + fi + done + printf '\n[NPU devfreq]\n' + shopt -s nullglob + npu_nodes=("$SYS_ROOT"/class/devfreq/*.npu) + shopt -u nullglob + for node in "${npu_nodes[@]}"; do + printf '%s\n' "${node##*/}" + for attribute in cur_freq min_freq max_freq governor available_frequencies; do + if [[ -r "$node/$attribute" ]]; then + printf ' %s=' "$attribute" + tr -d '\000' <"$node/$attribute" 2>/dev/null || true + printf '\n' + fi + done + done +} >"$REPORT/npu.txt" + +{ + printf '[ALSA cards]\n' + if [[ -r "$PROC_ROOT/asound/cards" ]]; then + cat "$PROC_ROOT/asound/cards" + else + printf 'unavailable\n' + fi +} >"$REPORT/audio.txt" +append_command audio.txt aplay -l +append_command audio.txt aplay -L + +{ + printf 'Interface state only; addresses and MACs are intentionally omitted.\n\n' + shopt -s nullglob + interfaces=("$SYS_ROOT"/class/net/*) + shopt -u nullglob + for interface in "${interfaces[@]}"; do + name=${interface##*/} + [[ "$name" == lo ]] && continue + printf '[%s]\n' "$name" + for attribute in operstate carrier speed duplex mtu; do + if [[ -r "$interface/$attribute" ]]; then + printf '%s=' "$attribute" + tr -d '\000' <"$interface/$attribute" 2>/dev/null || true + printf '\n' + fi + done + printf '\n' + done +} >"$REPORT/network.txt" + +run_capture storage.txt lsblk -o NAME,TYPE,SIZE,FSTYPE,MOUNTPOINTS,ROTA,TRAN +{ + printf '\n[root filesystem]\n' + if command -v findmnt >/dev/null 2>&1; then + findmnt -n -o SOURCE,FSTYPE / 2>&1 | sanitize_stream + else + printf 'findmnt unavailable\n' + fi +} >>"$REPORT/storage.txt" +run_capture usb.txt lsusb -t +append_command usb.txt lspci -nnk + +{ + if command -v dpkg-query >/dev/null 2>&1; then + dpkg-query -W -f='${binary:Package}\t${Version}\n' 2>/dev/null | + grep -Ei 'armbian|linux-image|linux-dtb|mesa|vulkan|kodi|ffmpeg|mpp|rga|rknn|alsa|libcec' | + sort || true + elif command -v rpm >/dev/null 2>&1; then + rpm -qa 2>/dev/null | + grep -Ei 'kernel|mesa|vulkan|kodi|ffmpeg|mpp|rga|rknn|alsa|libcec' | + sort || true + else + printf 'supported package inventory tool unavailable\n' + fi +} >"$REPORT/packages.txt" + +kernel_pattern='drm|hdmi|vop|edid|panthor|panfrost|mali|rknpu|npu|rkvdec|rkvenc|av1|mpp|rga|alsa|snd|cec|pcie|nvme|mmc' +{ + printf '[filtered current-boot kernel messages]\n' + if command -v journalctl >/dev/null 2>&1; then + timeout 45s journalctl -b -k --no-pager 2>&1 | + grep -Ei "$kernel_pattern" | tail -2500 || true + elif command -v dmesg >/dev/null 2>&1; then + dmesg --color=never 2>&1 | + grep -Ei "$kernel_pattern" | tail -2500 || true + else + printf 'kernel log reader unavailable\n' + fi +} | sanitize_stream >"$REPORT/kernel-media.log" + +if ((DIRECTORY_OUTPUT)); then + mkdir -- "$OUTPUT" + cp -a "$REPORT/." "$OUTPUT/" + chmod -R go-rwx "$OUTPUT" +else + command -v tar >/dev/null 2>&1 || die "tar is required for archive output" + tar -C "$REPORT" -czf "$OUTPUT" . + chmod 0600 "$OUTPUT" +fi + +printf 'Diagnostic bundle written to %s\n' "$OUTPUT" diff --git a/runtime/rk1-media-selftest b/runtime/rk1-media-selftest new file mode 100755 index 0000000..9a7bbf2 --- /dev/null +++ b/runtime/rk1-media-selftest @@ -0,0 +1,574 @@ +#!/usr/bin/env bash +# Read-only RK3588 media hardware inventory with optional short workloads. + +set -uo pipefail +export LC_ALL=C + +JSON=0 +QUICK=0 +STRICT=0 +MEDIA_DIR="" + +SYS_ROOT=${RK1_SYSFS_ROOT:-/sys} +PROC_ROOT=${RK1_PROCFS_ROOT:-/proc} +DEV_ROOT=${RK1_DEV_ROOT:-/dev} +RKNN_HOME=${RKNN_HOME:-/opt/rknn/current} + +declare -a CHECK_GROUP=() +declare -a CHECK_NAME=() +declare -a CHECK_STATUS=() +declare -a CHECK_MESSAGE=() + +usage() { + cat <<'EOF' +Usage: rk1-media-selftest [OPTIONS] + +Options: + --json Emit one JSON document instead of readable text. + --quick Inventory only; skip GPU, codec, and NPU workloads. + --strict Return nonzero for warnings as well as failures. + --media-dir PATH Exercise RKMPP decoding with named sample files. + Recognized names contain h264, hevc, vp9, or av1. + -h, --help Show this help. + +The default run is non-destructive. It executes only short inference, Vulkan, +and encoder probes when the relevant runtime and devices are present. It never +changes clocks, governors, display modes, storage, or network configuration. +EOF +} + +invocation_error() { + printf 'rk1-media-selftest: %s\n' "$*" >&2 + exit 2 +} + +while (($#)); do + case "$1" in + --json) + JSON=1 + shift + ;; + --quick) + QUICK=1 + shift + ;; + --strict) + STRICT=1 + shift + ;; + --media-dir) + (($# >= 2)) || invocation_error "--media-dir requires a path" + MEDIA_DIR=$2 + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + invocation_error "unknown option: $1" + ;; + esac +done + +if [[ -n "$MEDIA_DIR" && ! -d "$MEDIA_DIR" ]]; then + invocation_error "media directory is not a directory: $MEDIA_DIR" +fi + +add_check() { + CHECK_GROUP+=("$1") + CHECK_NAME+=("$2") + CHECK_STATUS+=("$3") + CHECK_MESSAGE+=("$4") +} + +read_text() { + local path=$1 + if [[ -r "$path" ]]; then + tr -d '\000' <"$path" 2>/dev/null || true + fi +} + +short_message() { + local value=$1 + value=${value//$'\r'/} + value=${value//$'\n'/'; '} + value=${value//$'\t'/ } + printf '%s' "${value:0:320}" +} + +driver_binding_count() { + local pattern=$1 + local directory entry base + local count=0 + shopt -s nullglob + for directory in "$SYS_ROOT"/bus/platform/drivers/$pattern; do + [[ -d "$directory" ]] || continue + for entry in "$directory"/*; do + base=${entry##*/} + case "$base" in + bind|unbind|uevent|module|new_id|remove_id) continue ;; + esac + if [[ -L "$entry" || -d "$entry" ]]; then + ((count++)) + fi + done + done + shopt -u nullglob + printf '%d' "$count" +} + +find_sample() { + local token=$1 + [[ -n "$MEDIA_DIR" ]] || return 1 + find "$MEDIA_DIR" -maxdepth 1 -type f \ + \( -iname "*${token}*.mkv" -o -iname "*${token}*.mp4" \ + -o -iname "*${token}*.webm" -o -iname "*${token}*.ts" \) \ + -print -quit 2>/dev/null +} + +# System identity +machine=$(uname -m 2>/dev/null || printf unknown) +if [[ "$machine" == "aarch64" ]]; then + add_check system architecture pass "aarch64 userspace" +else + add_check system architecture fail "expected aarch64, found $machine" +fi + +model=$(read_text "$PROC_ROOT/device-tree/model") +if [[ "$model" == *"Turing"*"RK1"* ]]; then + add_check system board pass "$model" +elif [[ -n "$model" ]]; then + add_check system board warn "device-tree model is $model" +else + add_check system board warn "device-tree model is unavailable" +fi + +# HDMI / DRM connector state +shopt -s nullglob +hdmi_connectors=("$SYS_ROOT"/class/drm/card*-HDMI-A-*) +shopt -u nullglob +if ((${#hdmi_connectors[@]} == 0)); then + add_check hdmi connector warn "no DRM HDMI connector is registered" +else + connected_count=0 + for connector in "${hdmi_connectors[@]}"; do + connector_name=${connector##*/} + state=$(read_text "$connector/status") + case "$state" in + connected) + ((connected_count++)) + add_check hdmi "$connector_name" pass "connected" + if [[ -s "$connector/edid" ]]; then + edid_bytes=$(wc -c <"$connector/edid" 2>/dev/null || printf 0) + add_check hdmi "$connector_name-edid" pass \ + "$edid_bytes bytes of EDID data" + else + add_check hdmi "$connector_name-edid" warn \ + "connected but EDID is empty or unreadable" + fi + if [[ -r "$connector/modes" ]] && + grep -qx '3840x2160' "$connector/modes" 2>/dev/null; then + add_check hdmi "$connector_name-4k" pass \ + "EDID advertises a 3840x2160 mode" + else + add_check hdmi "$connector_name-4k" warn \ + "3840x2160 is not present in the connector mode list" + fi + ;; + disconnected) + add_check hdmi "$connector_name" warn \ + "disconnected (expected when no display is attached)" + ;; + *) + add_check hdmi "$connector_name" warn \ + "connector state is ${state:-unknown}" + ;; + esac + done +fi + +hdmi_driver_count=$(driver_binding_count 'dwhdmi*') +hdmi_phy_count=$(driver_binding_count '*hdptx*hdmi*') +if ((hdmi_driver_count > 0 && hdmi_phy_count > 0)); then + add_check hdmi drivers pass \ + "$hdmi_driver_count controller and $hdmi_phy_count HDMI PHY binding(s)" +else + add_check hdmi drivers warn \ + "HDMI controller bindings=$hdmi_driver_count, PHY bindings=$hdmi_phy_count" +fi + +# GPU / Vulkan +gpu_driver="" +for candidate in panthor panfrost mali; do + if (($(driver_binding_count "$candidate") > 0)); then + gpu_driver=$candidate + break + fi +done +case "$gpu_driver" in + panthor) + add_check gpu kernel-driver pass "Panthor is bound to the GPU" + ;; + panfrost) + add_check gpu kernel-driver warn "Panfrost is bound; the image expects Panthor" + ;; + mali) + add_check gpu kernel-driver warn "proprietary Mali kernel driver is bound" + ;; + *) + add_check gpu kernel-driver warn "no bound Mali GPU driver was found" + ;; +esac + +shopt -s nullglob +gpu_devfreq=("$SYS_ROOT"/class/devfreq/*.gpu) +render_nodes=("$DEV_ROOT"/dri/renderD*) +shopt -u nullglob +if ((${#gpu_devfreq[@]} > 0)); then + add_check gpu devfreq pass "${#gpu_devfreq[@]} GPU devfreq device(s)" +else + add_check gpu devfreq warn "GPU devfreq node is missing" +fi +if ((${#render_nodes[@]} > 0)); then + add_check gpu render-node pass "${#render_nodes[@]} DRM render node(s)" +else + add_check gpu render-node warn "no accessible DRM render node" +fi + +if ((QUICK)); then + add_check gpu vulkan skip "active Vulkan probe disabled by --quick" +elif command -v vulkaninfo >/dev/null 2>&1; then + vulkan_output=$(timeout 20s vulkaninfo --summary 2>&1) + vulkan_rc=$? + if ((vulkan_rc == 0)); then + gpu_name=$(printf '%s\n' "$vulkan_output" | + sed -n 's/^[[:space:]]*deviceName[[:space:]]*=[[:space:]]*//p' | + head -1) + add_check gpu vulkan pass "${gpu_name:-vulkaninfo completed}" + else + add_check gpu vulkan fail \ + "vulkaninfo failed: $(short_message "$vulkan_output")" + fi +else + add_check gpu vulkan skip "vulkaninfo is not installed" +fi + +# VPU bindings and FFmpeg integration +decoder_bindings=$(driver_binding_count 'mpp_rkvdec*') +encoder_bindings=$(driver_binding_count 'mpp_rkvenc*') +av1_bindings=$(driver_binding_count '*av1*') +if ((decoder_bindings > 0)); then + add_check vpu decoder-driver pass "$decoder_bindings RKMPP decoder binding(s)" +else + add_check vpu decoder-driver warn "RKMPP decoder bindings are missing" +fi +if ((encoder_bindings > 0)); then + add_check vpu encoder-driver pass "$encoder_bindings RKMPP encoder binding(s)" +else + add_check vpu encoder-driver warn "RKMPP encoder bindings are missing" +fi +if ((av1_bindings > 0)); then + add_check vpu av1-driver pass "$av1_bindings AV1 decoder binding(s)" +else + add_check vpu av1-driver warn "a separate AV1 driver binding was not found" +fi + +FFMPEG="" +for candidate in /opt/rkmedia/bin/ffmpeg-rk /usr/local/bin/ffmpeg-rk; do + if [[ -x "$candidate" ]]; then + FFMPEG=$candidate + break + fi +done +if [[ -z "$FFMPEG" ]] && command -v ffmpeg-rk >/dev/null 2>&1; then + FFMPEG=$(command -v ffmpeg-rk) +fi + +if [[ -z "$FFMPEG" ]]; then + add_check vpu ffmpeg-rkmpp skip "ffmpeg-rk is not installed" + add_check rga ffmpeg-filter skip "ffmpeg-rk is not installed" +else + hwaccels=$($FFMPEG -hide_banner -hwaccels 2>&1) + if grep -qE '(^|[[:space:]])rkmpp($|[[:space:]])' <<<"$hwaccels"; then + add_check vpu ffmpeg-rkmpp pass "$FFMPEG advertises the rkmpp hwaccel" + else + add_check vpu ffmpeg-rkmpp fail "$FFMPEG does not advertise rkmpp" + fi + + encoders=$($FFMPEG -hide_banner -encoders 2>&1) + for codec in h264 hevc mjpeg; do + if grep -q "${codec}_rkmpp" <<<"$encoders"; then + add_check vpu "encode-$codec-capability" pass \ + "${codec}_rkmpp is registered" + else + add_check vpu "encode-$codec-capability" fail \ + "${codec}_rkmpp is not registered" + fi + done + + filters=$($FFMPEG -hide_banner -filters 2>&1) + if grep -q 'scale_rkrga' <<<"$filters"; then + add_check rga ffmpeg-filter pass "scale_rkrga is registered" + else + add_check rga ffmpeg-filter fail "scale_rkrga is not registered" + fi + + if ((QUICK)); then + add_check vpu encode-workload skip "active encoder probe disabled by --quick" + else + for codec in h264 hevc mjpeg; do + encode_output=$(timeout 30s "$FFMPEG" -nostdin -v error \ + -f lavfi -i 'color=size=320x240:rate=30:duration=0.2' \ + -frames:v 3 -c:v "${codec}_rkmpp" -f null - 2>&1) + encode_rc=$? + if ((encode_rc == 0)); then + add_check vpu "encode-$codec-workload" pass \ + "three frames submitted successfully" + else + add_check vpu "encode-$codec-workload" fail \ + "hardware encode failed: $(short_message "$encode_output")" + fi + done + fi + + if [[ -n "$MEDIA_DIR" && $QUICK -eq 0 ]]; then + first_decode_sample="" + for codec in h264 hevc vp9 av1; do + sample=$(find_sample "$codec" || true) + if [[ -z "$sample" ]]; then + add_check vpu "decode-$codec-workload" skip \ + "no sample containing '$codec' in $MEDIA_DIR" + continue + fi + [[ -n "$first_decode_sample" ]] || first_decode_sample=$sample + decode_output=$(timeout 180s "$FFMPEG" -nostdin -v error \ + -hwaccel rkmpp -i "$sample" -map 0:v:0 -f null - 2>&1) + decode_rc=$? + if ((decode_rc == 0)); then + add_check vpu "decode-$codec-workload" pass \ + "decoded ${sample##*/} through RKMPP" + else + add_check vpu "decode-$codec-workload" fail \ + "decode failed: $(short_message "$decode_output")" + fi + done + + if [[ -n "$first_decode_sample" ]] && grep -q 'scale_rkrga' <<<"$filters"; then + rga_output=$(timeout 60s "$FFMPEG" -nostdin -v error \ + -hwaccel rkmpp -hwaccel_output_format drm_prime \ + -i "$first_decode_sample" -map 0:v:0 -frames:v 30 \ + -vf 'scale_rkrga=w=1280:h=720:format=nv12' -f null - 2>&1) + rga_rc=$? + if ((rga_rc == 0)); then + add_check rga workload pass "RKMPP-to-RGA scaling completed" + else + add_check rga workload fail \ + "RGA workload failed: $(short_message "$rga_output")" + fi + else + add_check rga workload skip "no decode sample is available for RGA" + fi + elif [[ -z "$MEDIA_DIR" ]]; then + add_check vpu decode-workload skip "use --media-dir to test hardware decoding" + add_check rga workload skip "use --media-dir to test zero-copy RGA scaling" + fi +fi + +rga2_bindings=$(driver_binding_count 'rga2') +rga3_bindings=$(driver_binding_count 'rga3') +if ((rga2_bindings + rga3_bindings > 0)); then + add_check rga kernel-driver pass \ + "RGA2 bindings=$rga2_bindings, RGA3 bindings=$rga3_bindings" +else + add_check rga kernel-driver warn "no RGA2/RGA3 binding was found" +fi + +# NPU runtime and per-core inference +npu_bindings=$(driver_binding_count 'rknpu') +((npu_bindings += $(driver_binding_count 'RKNPU'))) +shopt -s nullglob +npu_devfreq=("$SYS_ROOT"/class/devfreq/*.npu) +shopt -u nullglob +if ((npu_bindings > 0 || ${#npu_devfreq[@]} > 0)); then + add_check npu kernel-driver pass \ + "RKNPU bindings=$npu_bindings, devfreq nodes=${#npu_devfreq[@]}" + npu_present=1 +else + add_check npu kernel-driver warn "RKNPU driver/device was not found" + npu_present=0 +fi + +runtime_so="$RKNN_HOME/lib/librknnrt.so" +model_path="$RKNN_HOME/share/models/rk3588/mobilenet_v1.rknn" +npu_test="$RKNN_HOME/bin/rknn-inference-test" +if [[ -r "$runtime_so" ]]; then + runtime_hash=$(sha256sum "$runtime_so" 2>/dev/null) + runtime_hash=${runtime_hash%% *} + if [[ "$runtime_hash" == \ + "d31fc19c85b85f6091b2bd0f6af9d962d5264a4e410bfb536402ec92bac738e8" ]]; then + add_check npu runtime pass "RKNN Runtime 2.3.2 checksum matches" + else + add_check npu runtime fail "unexpected librknnrt.so checksum $runtime_hash" + fi +else + add_check npu runtime warn "RKNN runtime is not installed at $runtime_so" +fi + +if ((QUICK)); then + add_check npu inference skip "active inference disabled by --quick" +elif ((npu_present == 0)); then + add_check npu inference skip "NPU driver is absent; inference was not attempted" +elif [[ ! -x "$npu_test" || ! -r "$model_path" ]]; then + add_check npu inference skip "test binary or pinned RK3588 model is unavailable" +else + for core in 0 1 2 all; do + npu_output=$(timeout 45s "$npu_test" --model "$model_path" \ + --core "$core" --iterations 1 2>&1) + npu_rc=$? + if ((npu_rc == 0)) && grep -q 'RKNN_RESULT status=pass' <<<"$npu_output"; then + add_check npu "core-$core" pass "$(short_message "$npu_output")" + else + add_check npu "core-$core" fail \ + "inference failed: $(short_message "$npu_output")" + fi + done +fi + +# HDMI ALSA devices +asound_cards=$(read_text "$PROC_ROOT/asound/cards") +if [[ -n "$asound_cards" ]] && grep -qiE 'hdmi|rockchiphdmi' <<<"$asound_cards"; then + add_check audio hdmi-card pass "an HDMI ALSA card is registered" +elif [[ -n "$asound_cards" ]]; then + add_check audio hdmi-card warn "ALSA cards exist, but none is labeled HDMI" +else + add_check audio hdmi-card warn "ALSA card inventory is unavailable" +fi +if command -v aplay >/dev/null 2>&1; then + aplay_output=$(aplay -l 2>&1) + if grep -qi 'hdmi' <<<"$aplay_output"; then + add_check audio pcm-device pass "aplay lists an HDMI PCM device" + else + add_check audio pcm-device warn \ + "aplay did not list an HDMI PCM device: $(short_message "$aplay_output")" + fi +else + add_check audio pcm-device skip "aplay is not installed" +fi + +# Link state only: no pings, DNS requests, or network mutations. +shopt -s nullglob +interfaces=("$SYS_ROOT"/class/net/*) +shopt -u nullglob +interface_names=() +up_names=() +for interface in "${interfaces[@]}"; do + name=${interface##*/} + [[ "$name" == lo ]] && continue + interface_names+=("$name") + state=$(read_text "$interface/operstate") + carrier=$(read_text "$interface/carrier") + if [[ "$state" == up || "$carrier" == 1 ]]; then + up_names+=("$name") + fi +done +if ((${#interface_names[@]} == 0)); then + add_check network interfaces warn "no non-loopback interface was found" +elif ((${#up_names[@]} > 0)); then + add_check network link pass "up: ${up_names[*]}" +else + add_check network link warn \ + "interfaces present but down: ${interface_names[*]}" +fi + +# Root filesystem, eMMC, and NVMe presence. No serial numbers are read. +if command -v findmnt >/dev/null 2>&1; then + root_source=$(findmnt -n -o SOURCE / 2>/dev/null || true) + root_fstype=$(findmnt -n -o FSTYPE / 2>/dev/null || true) + if [[ -n "$root_source" ]]; then + add_check storage rootfs pass "$root_source ($root_fstype)" + else + add_check storage rootfs warn "root filesystem source is unavailable" + fi +else + add_check storage rootfs skip "findmnt is not installed" +fi + +shopt -s nullglob +emmc_devices=("$SYS_ROOT"/block/mmcblk*) +nvme_devices=("$SYS_ROOT"/block/nvme*n1) +shopt -u nullglob +if ((${#emmc_devices[@]} > 0)); then + add_check storage emmc pass "${#emmc_devices[@]} MMC block device(s)" +else + add_check storage emmc warn "no MMC/eMMC block device was found" +fi +if ((${#nvme_devices[@]} > 0)); then + add_check storage nvme pass "${#nvme_devices[@]} NVMe namespace(s)" +else + add_check storage nvme warn "no NVMe namespace was found" +fi + +pass_count=0 +warn_count=0 +fail_count=0 +skip_count=0 +for status in "${CHECK_STATUS[@]}"; do + case "$status" in + pass) ((pass_count++)) ;; + warn) ((warn_count++)) ;; + fail) ((fail_count++)) ;; + skip) ((skip_count++)) ;; + esac +done + +if ((fail_count > 0)); then + overall=fail +elif ((warn_count > 0)); then + overall=warn +else + overall=pass +fi + +json_escape() { + local value=$1 + value=${value//\\/\\\\} + value=${value//\"/\\\"} + value=${value//$'\n'/\\n} + value=${value//$'\r'/\\r} + value=${value//$'\t'/\\t} + printf '%s' "$value" +} + +if ((JSON)); then + printf '{"schema_version":1,"overall":"%s","quick":%s,' \ + "$overall" "$([[ $QUICK -eq 1 ]] && printf true || printf false)" + printf '"summary":{"pass":%d,"warn":%d,"fail":%d,"skip":%d},' \ + "$pass_count" "$warn_count" "$fail_count" "$skip_count" + printf '"checks":[' + for ((i = 0; i < ${#CHECK_NAME[@]}; ++i)); do + ((i == 0)) || printf ',' + printf '{"group":"%s","name":"%s","status":"%s","message":"%s"}' \ + "$(json_escape "${CHECK_GROUP[i]}")" \ + "$(json_escape "${CHECK_NAME[i]}")" \ + "${CHECK_STATUS[i]}" \ + "$(json_escape "${CHECK_MESSAGE[i]}")" + done + printf ']}\n' +else + printf '%-9s %-24s %-6s %s\n' GROUP CHECK STATUS DETAIL + printf '%-9s %-24s %-6s %s\n' '---------' '------------------------' \ + '------' '------' + for ((i = 0; i < ${#CHECK_NAME[@]}; ++i)); do + printf '%-9s %-24s %-6s %s\n' \ + "${CHECK_GROUP[i]}" "${CHECK_NAME[i]}" \ + "${CHECK_STATUS[i]^^}" "${CHECK_MESSAGE[i]}" + done + printf '\nOverall: %s (pass=%d warn=%d fail=%d skip=%d)\n' \ + "${overall^^}" "$pass_count" "$warn_count" "$fail_count" "$skip_count" +fi + +if ((fail_count > 0 || (STRICT && warn_count > 0))); then + exit 1 +fi +exit 0 diff --git a/runtime/rknn-inference-test.c b/runtime/rknn-inference-test.c new file mode 100644 index 0000000..cc17baa --- /dev/null +++ b/runtime/rknn-inference-test.c @@ -0,0 +1,315 @@ +/* + * Minimal RK3588 NPU smoke test for RKNN Runtime 2.3.2. + * + * The program deliberately uses deterministic synthetic inputs. Its purpose is + * to verify model loading, core selection, command submission, and finite + * output, not the semantic accuracy of MobileNet. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "rknn_api.h" + +#define DEFAULT_MODEL "/opt/rknn/current/share/models/rk3588/mobilenet_v1.rknn" +#define MAX_TENSORS 64U + +static void usage(FILE *stream, const char *program) +{ + fprintf(stream, + "Usage: %s [--model PATH] [--core auto|0|1|2|all] " + "[--iterations N]\n", + program); +} + +static int parse_core(const char *value, rknn_core_mask *mask) +{ + if (strcmp(value, "auto") == 0) { + *mask = RKNN_NPU_CORE_AUTO; + } else if (strcmp(value, "0") == 0) { + *mask = RKNN_NPU_CORE_0; + } else if (strcmp(value, "1") == 0) { + *mask = RKNN_NPU_CORE_1; + } else if (strcmp(value, "2") == 0) { + *mask = RKNN_NPU_CORE_2; + } else if (strcmp(value, "all") == 0) { + *mask = RKNN_NPU_CORE_0_1_2; + } else { + return -1; + } + return 0; +} + +static int load_file(const char *path, void **buffer, uint32_t *size) +{ + struct stat info; + FILE *file = NULL; + void *data = NULL; + + if (stat(path, &info) != 0) { + fprintf(stderr, "cannot stat model %s: %s\n", path, strerror(errno)); + return -1; + } + if (info.st_size <= 0 || (uint64_t)info.st_size > UINT32_MAX) { + fprintf(stderr, "invalid model size: %jd\n", (intmax_t)info.st_size); + return -1; + } + + file = fopen(path, "rb"); + if (file == NULL) { + fprintf(stderr, "cannot open model %s: %s\n", path, strerror(errno)); + return -1; + } + data = malloc((size_t)info.st_size); + if (data == NULL) { + fprintf(stderr, "cannot allocate %jd bytes for model\n", + (intmax_t)info.st_size); + fclose(file); + return -1; + } + if (fread(data, 1, (size_t)info.st_size, file) != (size_t)info.st_size) { + fprintf(stderr, "short read from model %s\n", path); + free(data); + fclose(file); + return -1; + } + fclose(file); + *buffer = data; + *size = (uint32_t)info.st_size; + return 0; +} + +static double elapsed_ms(const struct timespec *start, const struct timespec *end) +{ + double seconds = (double)(end->tv_sec - start->tv_sec) * 1000.0; + double nanos = (double)(end->tv_nsec - start->tv_nsec) / 1000000.0; + return seconds + nanos; +} + +int main(int argc, char **argv) +{ + const char *model_path = DEFAULT_MODEL; + const char *core_name = "auto"; + rknn_core_mask core_mask = RKNN_NPU_CORE_AUTO; + unsigned long iterations = 1; + void *model = NULL; + uint32_t model_size = 0; + rknn_context context = 0; + rknn_sdk_version sdk_version; + rknn_input_output_num io_count; + rknn_tensor_attr *input_attrs = NULL; + rknn_input *inputs = NULL; + rknn_output *outputs = NULL; + int outputs_acquired = 0; + int context_created = 0; + int result = EXIT_FAILURE; + double total_ms = 0.0; + uint32_t last_top_index = 0; + float last_top_value = -INFINITY; + int ret; + uint32_t i; + + for (i = 1; i < (uint32_t)argc; ++i) { + if (strcmp(argv[i], "--model") == 0 && i + 1U < (uint32_t)argc) { + model_path = argv[++i]; + } else if (strcmp(argv[i], "--core") == 0 && i + 1U < (uint32_t)argc) { + core_name = argv[++i]; + if (parse_core(core_name, &core_mask) != 0) { + fprintf(stderr, "invalid core selector: %s\n", core_name); + usage(stderr, argv[0]); + return 2; + } + } else if (strcmp(argv[i], "--iterations") == 0 && + i + 1U < (uint32_t)argc) { + char *end = NULL; + errno = 0; + iterations = strtoul(argv[++i], &end, 10); + if (errno != 0 || end == argv[i] || *end != '\0' || + iterations == 0 || iterations > 1000) { + fprintf(stderr, "iterations must be between 1 and 1000\n"); + return 2; + } + } else if (strcmp(argv[i], "--help") == 0 || + strcmp(argv[i], "-h") == 0) { + usage(stdout, argv[0]); + return 0; + } else { + fprintf(stderr, "unknown or incomplete option: %s\n", argv[i]); + usage(stderr, argv[0]); + return 2; + } + } + + if (load_file(model_path, &model, &model_size) != 0) { + goto cleanup; + } + + ret = rknn_init(&context, model, model_size, 0, NULL); + if (ret != RKNN_SUCC) { + fprintf(stderr, "rknn_init failed: %d\n", ret); + goto cleanup; + } + context_created = 1; + + ret = rknn_set_core_mask(context, core_mask); + if (ret != RKNN_SUCC) { + fprintf(stderr, "rknn_set_core_mask(%s) failed: %d\n", core_name, ret); + goto cleanup; + } + + memset(&sdk_version, 0, sizeof(sdk_version)); + ret = rknn_query(context, RKNN_QUERY_SDK_VERSION, &sdk_version, + sizeof(sdk_version)); + if (ret != RKNN_SUCC) { + fprintf(stderr, "RKNN_QUERY_SDK_VERSION failed: %d\n", ret); + goto cleanup; + } + + memset(&io_count, 0, sizeof(io_count)); + ret = rknn_query(context, RKNN_QUERY_IN_OUT_NUM, &io_count, + sizeof(io_count)); + if (ret != RKNN_SUCC || io_count.n_input == 0 || io_count.n_output == 0 || + io_count.n_input > MAX_TENSORS || io_count.n_output > MAX_TENSORS) { + fprintf(stderr, "invalid RKNN input/output count (%u/%u), ret=%d\n", + io_count.n_input, io_count.n_output, ret); + goto cleanup; + } + + input_attrs = calloc(io_count.n_input, sizeof(*input_attrs)); + inputs = calloc(io_count.n_input, sizeof(*inputs)); + outputs = calloc(io_count.n_output, sizeof(*outputs)); + if (input_attrs == NULL || inputs == NULL || outputs == NULL) { + fprintf(stderr, "cannot allocate tensor metadata\n"); + goto cleanup; + } + + for (i = 0; i < io_count.n_input; ++i) { + uint32_t byte; + input_attrs[i].index = i; + ret = rknn_query(context, RKNN_QUERY_INPUT_ATTR, &input_attrs[i], + sizeof(input_attrs[i])); + if (ret != RKNN_SUCC || input_attrs[i].n_elems == 0) { + fprintf(stderr, "query for input %u failed: %d\n", i, ret); + goto cleanup; + } + + inputs[i].index = i; + inputs[i].size = input_attrs[i].n_elems; + inputs[i].type = RKNN_TENSOR_UINT8; + inputs[i].fmt = input_attrs[i].fmt == RKNN_TENSOR_UNDEFINED + ? RKNN_TENSOR_NHWC + : input_attrs[i].fmt; + inputs[i].pass_through = 0; + inputs[i].buf = malloc(inputs[i].size); + if (inputs[i].buf == NULL) { + fprintf(stderr, "cannot allocate input %u (%u bytes)\n", i, + inputs[i].size); + goto cleanup; + } + for (byte = 0; byte < inputs[i].size; ++byte) { + ((uint8_t *)inputs[i].buf)[byte] = + (uint8_t)((byte * 17U + i * 23U) & 0xffU); + } + } + + ret = rknn_inputs_set(context, io_count.n_input, inputs); + if (ret != RKNN_SUCC) { + fprintf(stderr, "rknn_inputs_set failed: %d\n", ret); + goto cleanup; + } + + for (i = 0; i < io_count.n_output; ++i) { + outputs[i].index = i; + outputs[i].want_float = 1; + outputs[i].is_prealloc = 0; + } + + for (unsigned long iteration = 0; iteration < iterations; ++iteration) { + struct timespec start; + struct timespec end; + + if (clock_gettime(CLOCK_MONOTONIC, &start) != 0) { + fprintf(stderr, "clock_gettime failed: %s\n", strerror(errno)); + goto cleanup; + } + ret = rknn_run(context, NULL); + if (ret != RKNN_SUCC) { + fprintf(stderr, "rknn_run failed at iteration %lu: %d\n", + iteration, ret); + goto cleanup; + } + ret = rknn_outputs_get(context, io_count.n_output, outputs, NULL); + if (ret != RKNN_SUCC) { + fprintf(stderr, "rknn_outputs_get failed at iteration %lu: %d\n", + iteration, ret); + goto cleanup; + } + outputs_acquired = 1; + if (clock_gettime(CLOCK_MONOTONIC, &end) != 0) { + fprintf(stderr, "clock_gettime failed: %s\n", strerror(errno)); + goto cleanup; + } + total_ms += elapsed_ms(&start, &end); + + last_top_value = -INFINITY; + last_top_index = 0; + for (i = 0; i < io_count.n_output; ++i) { + const float *values = outputs[i].buf; + uint32_t count = outputs[i].size / (uint32_t)sizeof(float); + uint32_t value_index; + if (values == NULL || count == 0) { + fprintf(stderr, "output %u is empty\n", i); + goto cleanup; + } + for (value_index = 0; value_index < count; ++value_index) { + if (!isfinite(values[value_index])) { + fprintf(stderr, "output %u contains a non-finite value\n", i); + goto cleanup; + } + if (i == 0 && values[value_index] > last_top_value) { + last_top_value = values[value_index]; + last_top_index = value_index; + } + } + } + + ret = rknn_outputs_release(context, io_count.n_output, outputs); + outputs_acquired = 0; + if (ret != RKNN_SUCC) { + fprintf(stderr, "rknn_outputs_release failed: %d\n", ret); + goto cleanup; + } + } + + printf("RKNN_RESULT status=pass core=%s iterations=%lu avg_ms=%.3f " + "top_index=%u top_value=%.7g api=%s driver=%s\n", + core_name, iterations, total_ms / (double)iterations, + last_top_index, last_top_value, sdk_version.api_version, + sdk_version.drv_version); + result = EXIT_SUCCESS; + +cleanup: + if (outputs_acquired) { + (void)rknn_outputs_release(context, io_count.n_output, outputs); + } + if (inputs != NULL) { + for (i = 0; i < io_count.n_input; ++i) { + free(inputs[i].buf); + } + } + free(outputs); + free(inputs); + free(input_attrs); + if (context_created) { + (void)rknn_destroy(context); + } + free(model); + return result; +} diff --git a/runtime/rknn-version.env b/runtime/rknn-version.env new file mode 100644 index 0000000..d6a6796 --- /dev/null +++ b/runtime/rknn-version.env @@ -0,0 +1,12 @@ +# Pinned local-inference runtime for the RK3588 NPU. +# This file is sourced by install-rknn-runtime.sh. +RKNN_VERSION="2.3.2" +RKNN_TAG="v2.3.2" +RKNN_COMMIT="42aa1d426c0a9e0869b6374edba009f7208a1926" +RKNN_REPOSITORY="https://github.com/airockchip/rknn-toolkit2.git" + +RKNN_HEADER_SHA256="c48e11a6f41b451a5fd1e4ad774ea60252d3d94f78bee9b21ea3d21b21deba9a" +RKNN_RUNTIME_AARCH64_SHA256="d31fc19c85b85f6091b2bd0f6af9d962d5264a4e410bfb536402ec92bac738e8" +RKNN_MOBILENET_RK3588_SHA256="381dae3b7038a98b10f6ec9dcdbb094a49247341856fb294e692ef518218fcfb" +RKNN_DEMO_IMAGE_SHA256="c350299c6283d5f62fecf1f845b6b3be9aafec8dff528ca09a129990f0a584b0" +RKNN_LICENSE_SHA256="d846f57d942c7dfdca7b8b54f9e8bb39e1e226790dc4f5ee205d6fd678961720" diff --git a/runtime/tests/run-tests.sh b/runtime/tests/run-tests.sh new file mode 100755 index 0000000..7f6c678 --- /dev/null +++ b/runtime/tests/run-tests.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +TEST_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) +RUNTIME_DIR=$(cd -- "$TEST_DIR/.." && pwd -P) +TEST_TMP=$(mktemp -d "${TMPDIR:-/tmp}/rk1-runtime-tests.XXXXXXXX") + +cleanup() { + rm -rf -- "$TEST_TMP" +} +trap cleanup EXIT + +fail() { + printf 'FAIL: %s\n' "$*" >&2 + exit 1 +} + +pass() { + printf 'PASS: %s\n' "$*" +} + +bash -n \ + "$RUNTIME_DIR/install-rknn-runtime.sh" \ + "$RUNTIME_DIR/rk1-media-selftest" \ + "$RUNTIME_DIR/rk1-media-diagnostics" +pass "shell syntax" + +"$RUNTIME_DIR/install-rknn-runtime.sh" --help >/dev/null +"$RUNTIME_DIR/rk1-media-selftest" --help >/dev/null +"$RUNTIME_DIR/rk1-media-diagnostics" --help >/dev/null +pass "command help" + +mkdir -p \ + "$TEST_TMP/empty/sys" \ + "$TEST_TMP/empty/proc" \ + "$TEST_TMP/empty/dev" \ + "$TEST_TMP/empty/rknn" +RK1_SYSFS_ROOT="$TEST_TMP/empty/sys" \ +RK1_PROCFS_ROOT="$TEST_TMP/empty/proc" \ +RK1_DEV_ROOT="$TEST_TMP/empty/dev" \ +RKNN_HOME="$TEST_TMP/empty/rknn" \ + "$RUNTIME_DIR/rk1-media-selftest" --quick --json \ + >"$TEST_TMP/empty.json" +grep -q '"overall":"warn"' "$TEST_TMP/empty.json" || + fail "empty-hardware JSON does not report a warning" +grep -q '"checks":\[' "$TEST_TMP/empty.json" || + fail "empty-hardware JSON has no checks" +pass "absent hardware degrades safely" + +FAKE="$TEST_TMP/fake" +mkdir -p \ + "$FAKE/sys/class/drm/card0-HDMI-A-1" \ + "$FAKE/sys/class/devfreq/fb000000.gpu" \ + "$FAKE/sys/class/devfreq/fdab0000.npu" \ + "$FAKE/sys/class/net/eth0" \ + "$FAKE/sys/block/mmcblk0" \ + "$FAKE/sys/block/nvme0n1" \ + "$FAKE/sys/bus/platform/drivers/dwhdmi-rockchip/fde80000.hdmi" \ + "$FAKE/sys/bus/platform/drivers/rockchip-hdptx-phy-hdmi/fed60000.hdmiphy" \ + "$FAKE/sys/bus/platform/drivers/panthor/fb000000.gpu" \ + "$FAKE/sys/bus/platform/drivers/mpp_rkvdec2/fdc38100.rkvdec-core" \ + "$FAKE/sys/bus/platform/drivers/mpp_rkvenc2/fdbd0000.rkvenc-core" \ + "$FAKE/sys/bus/platform/drivers/mpp_av1dec/av1-decoder" \ + "$FAKE/sys/bus/platform/drivers/rga2/fdb80000.rga" \ + "$FAKE/sys/bus/platform/drivers/rga3/fdb60000.rga" \ + "$FAKE/sys/bus/platform/drivers/RKNPU/fdab0000.npu" \ + "$FAKE/proc/device-tree" \ + "$FAKE/proc/asound" \ + "$FAKE/dev/dri" \ + "$FAKE/rknn" +printf 'connected\n' >"$FAKE/sys/class/drm/card0-HDMI-A-1/status" +printf '3840x2160\n1920x1080\n' >"$FAKE/sys/class/drm/card0-HDMI-A-1/modes" +printf 'fake-edid' >"$FAKE/sys/class/drm/card0-HDMI-A-1/edid" +printf 'Turing Machines RK1\0' >"$FAKE/proc/device-tree/model" +printf ' 0 [rockchiphdmi]: rockchip-hdmi - rockchip-hdmi\n' \ + >"$FAKE/proc/asound/cards" +printf 'up\n' >"$FAKE/sys/class/net/eth0/operstate" +printf '1\n' >"$FAKE/sys/class/net/eth0/carrier" +touch "$FAKE/dev/dri/renderD128" + +RK1_SYSFS_ROOT="$FAKE/sys" \ +RK1_PROCFS_ROOT="$FAKE/proc" \ +RK1_DEV_ROOT="$FAKE/dev" \ +RKNN_HOME="$FAKE/rknn" \ + "$RUNTIME_DIR/rk1-media-selftest" --quick --json \ + >"$TEST_TMP/fake.json" +grep -q '"name":"card0-HDMI-A-1","status":"pass"' \ + "$TEST_TMP/fake.json" || fail "fake connected HDMI was not detected" +grep -q '"name":"kernel-driver","status":"pass","message":"Panthor' \ + "$TEST_TMP/fake.json" || fail "fake Panthor binding was not detected" +pass "synthetic hardware inventory" + +if command -v python3 >/dev/null 2>&1; then + python3 -m json.tool "$TEST_TMP/empty.json" >/dev/null + python3 -m json.tool "$TEST_TMP/fake.json" >/dev/null + pass "JSON validity" +fi + +RK1_SYSFS_ROOT="$FAKE/sys" \ +RK1_PROCFS_ROOT="$FAKE/proc" \ +RK1_DEV_ROOT="$FAKE/dev" \ +RKNN_HOME="$FAKE/rknn" \ + "$RUNTIME_DIR/rk1-media-diagnostics" --directory \ + --output "$TEST_TMP/diagnostics" >/dev/null +[[ -s "$TEST_TMP/diagnostics/selftest.json" ]] || + fail "diagnostic bundle is missing selftest.json" +[[ -s "$TEST_TMP/diagnostics/drm-connectors.txt" ]] || + fail "diagnostic bundle is missing connector data" +[[ -s "$TEST_TMP/diagnostics/kernel-media.log" ]] || + fail "diagnostic bundle is missing filtered kernel data" +pass "diagnostic collection" + +if [[ -n ${RKNN_SOURCE_DIR:-} ]]; then + [[ -d "$RKNN_SOURCE_DIR" ]] || fail "RKNN_SOURCE_DIR is not a directory" + bundled_source="$TEST_TMP/bundled-rknn-source" + mkdir -p \ + "$bundled_source/rknpu2/runtime/Linux/librknn_api/include" \ + "$bundled_source/rknpu2/runtime/Linux/librknn_api/aarch64" \ + "$bundled_source/rknpu2/examples/rknn_api_demo/model/RK3588" \ + "$bundled_source/rknpu2/examples/rknn_api_demo/model" + cp "$RKNN_SOURCE_DIR/LICENSE" "$bundled_source/LICENSE" + cp "$RKNN_SOURCE_DIR/rknpu2/runtime/Linux/librknn_api/include/rknn_api.h" \ + "$bundled_source/rknpu2/runtime/Linux/librknn_api/include/rknn_api.h" + cp "$RKNN_SOURCE_DIR/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so" \ + "$bundled_source/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so" + cp "$RKNN_SOURCE_DIR/rknpu2/examples/rknn_api_demo/model/RK3588/mobilenet_v1.rknn" \ + "$bundled_source/rknpu2/examples/rknn_api_demo/model/RK3588/mobilenet_v1.rknn" + cp "$RKNN_SOURCE_DIR/rknpu2/examples/rknn_api_demo/model/dog_224x224.jpg" \ + "$bundled_source/rknpu2/examples/rknn_api_demo/model/dog_224x224.jpg" + mkdir -p "$TEST_TMP/rootfs" + "$RUNTIME_DIR/install-rknn-runtime.sh" \ + --rootfs "$TEST_TMP/rootfs" --source-dir "$bundled_source" + installed_runtime="$TEST_TMP/rootfs/opt/rknn/2.3.2/lib/librknnrt.so" + installed_model="$TEST_TMP/rootfs/opt/rknn/2.3.2/share/models/rk3588/mobilenet_v1.rknn" + [[ -x "$TEST_TMP/rootfs/opt/rknn/2.3.2/bin/rknn-inference-test" ]] || + fail "compiled inference test is missing" + [[ $(sha256sum "$installed_runtime" | awk '{print $1}') == \ + d31fc19c85b85f6091b2bd0f6af9d962d5264a4e410bfb536402ec92bac738e8 ]] || + fail "installed runtime checksum differs" + [[ $(sha256sum "$installed_model" | awk '{print $1}') == \ + 381dae3b7038a98b10f6ec9dcdbb094a49247341856fb294e692ef518218fcfb ]] || + fail "installed model checksum differs" + pass "pinned RKNN installation" +else + printf 'SKIP: installer integration (set RKNN_SOURCE_DIR to a v2.3.2 checkout)\n' +fi + +printf 'All runtime tests passed.\n' diff --git a/scripts/fetch-inputs.sh b/scripts/fetch-inputs.sh new file mode 100755 index 0000000..ef89619 --- /dev/null +++ b/scripts/fetch-inputs.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# Fetch and verify every binary input required by the loopless image composer. + +set -Eeuo pipefail +umask 022 + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) +PROJECT_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd -P) +LOCK_FILE="$PROJECT_DIR/inputs.lock.json" +DOWNLOAD_DIR="$PROJECT_DIR/downloads" + +for command_name in curl git jq sha256sum stat; do + command -v "$command_name" >/dev/null || { + echo "Missing command: $command_name" >&2 + exit 1 + } +done +mkdir -p "$DOWNLOAD_DIR" "$PROJECT_DIR/work/sources" + +fetch_asset() { + local selector=$1 output_name url expected_size expected_sha output partial + output_name=$(jq -er "$selector.output_name" "$LOCK_FILE") + url=$(jq -er "$selector.url" "$LOCK_FILE") + expected_size=$(jq -er "$selector.size" "$LOCK_FILE") + expected_sha=$(jq -er "$selector.sha256" "$LOCK_FILE") + output="$DOWNLOAD_DIR/$output_name" + if [[ ! -f "$output" ]] || + [[ "$(stat -c '%s' "$output")" != "$expected_size" ]] || + [[ "$(sha256sum "$output" | awk '{print $1}')" != "$expected_sha" ]]; then + partial="$output.partial" + rm -f -- "$partial" + curl --fail --location --show-error --output "$partial" "$url" + [[ "$(stat -c '%s' "$partial")" == "$expected_size" ]] || { + echo "Size mismatch for $output_name" >&2 + exit 1 + } + echo "$expected_sha $partial" | sha256sum --check --status || { + echo "Checksum mismatch for $output_name" >&2 + exit 1 + } + mv -- "$partial" "$output" + fi + echo "Verified $output_name" +} + +verify_rknn_bundle() { + local bundle=$1 + # shellcheck source=../runtime/rknn-version.env + source "$PROJECT_DIR/runtime/rknn-version.env" + declare -a checks=( + "rknpu2/runtime/Linux/librknn_api/include/rknn_api.h:$RKNN_HEADER_SHA256" + "rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so:$RKNN_RUNTIME_AARCH64_SHA256" + "rknpu2/examples/rknn_api_demo/model/RK3588/mobilenet_v1.rknn:$RKNN_MOBILENET_RK3588_SHA256" + "rknpu2/examples/rknn_api_demo/model/dog_224x224.jpg:$RKNN_DEMO_IMAGE_SHA256" + "LICENSE:$RKNN_LICENSE_SHA256" + ) + local entry path expected + for entry in "${checks[@]}"; do + path=${entry%%:*} + expected=${entry##*:} + [[ -f "$bundle/$path" ]] || return 1 + [[ "$(sha256sum "$bundle/$path" | awk '{print $1}')" == "$expected" ]] || return 1 + done +} + +fetch_asset '.base_image' +fetch_asset '.kodi_upstream_deb' + +bundle_name=$(jq -er '.rknn_toolkit2.bundle_directory' "$LOCK_FILE") +bundle="$DOWNLOAD_DIR/$bundle_name" +if ! verify_rknn_bundle "$bundle"; then + [[ ! -e "$bundle" ]] || { + echo "Existing RKNN bundle failed verification: $bundle" >&2 + exit 1 + } + repository=$(jq -er '.rknn_toolkit2.repository' "$LOCK_FILE") + tag=$(jq -er '.rknn_toolkit2.tag' "$LOCK_FILE") + commit=$(jq -er '.rknn_toolkit2.commit' "$LOCK_FILE") + checkout="$PROJECT_DIR/work/sources/rknn-toolkit2-$commit" + if [[ ! -d "$checkout/.git" ]]; then + git clone --filter=blob:none --no-checkout "$repository" "$checkout" + git -C "$checkout" sparse-checkout init --cone + git -C "$checkout" sparse-checkout set \ + rknpu2/runtime/Linux/librknn_api \ + rknpu2/examples/rknn_api_demo/model + git -C "$checkout" checkout --detach "$tag" + fi + [[ "$(git -C "$checkout" rev-parse HEAD)" == "$commit" ]] || { + echo "RKNN checkout does not match lock" >&2 + exit 1 + } + "$PROJECT_DIR/runtime/make-offline-bundle.sh" "$checkout" "$bundle" +fi +verify_rknn_bundle "$bundle" || { echo "RKNN bundle verification failed" >&2; exit 1; } +echo "Verified $bundle_name" + +"$PROJECT_DIR/packages/fetch-locked-debs.sh" diff --git a/scripts/flash-emmc.sh b/scripts/flash-emmc.sh new file mode 100755 index 0000000..bd882cd --- /dev/null +++ b/scripts/flash-emmc.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Deliberately guarded writer for the completed RK1 eMMC image. + +set -Eeuo pipefail + +IMAGE="" +TARGET="" +CONFIRM="" + +usage() { + cat <<'EOF' +Usage: sudo ./scripts/flash-emmc.sh --image IMAGE.img.xz \ + --target /dev/mmcblkN --confirm /dev/mmcblkN + +The target and confirmation must match exactly. Only whole mmcblk devices are +accepted, and a device with mounted children or active swap is rejected. +EOF +} + +while (($#)); do + case "$1" in + --image) IMAGE=${2:?}; shift 2 ;; + --target) TARGET=${2:?}; shift 2 ;; + --confirm) CONFIRM=${2:?}; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;; + esac +done + +for command_name in blockdev cmp dd lsblk sha256sum xz; do + command -v "$command_name" >/dev/null || { + echo "Missing command: $command_name" >&2 + exit 1 + } +done +[[ ${EUID:-$(id -u)} -eq 0 ]] || { echo 'Run this command as root' >&2; exit 1; } +[[ -f "$IMAGE" && "$IMAGE" == *.img.xz ]] || { echo 'A .img.xz image is required' >&2; exit 2; } +TARGET=$(readlink -f -- "$TARGET") +[[ "$TARGET" =~ ^/dev/mmcblk[0-9]+$ ]] || { + echo 'Target must be a whole /dev/mmcblkN device (never a partition)' >&2 + exit 2 +} +[[ "$CONFIRM" == "$TARGET" ]] || { echo '--confirm must exactly repeat the resolved target' >&2; exit 2; } +[[ -b "$TARGET" && "$(lsblk -dnro TYPE "$TARGET")" == disk ]] || { + echo "Not a whole block device: $TARGET" >&2 + exit 1 +} + +if lsblk -nrpo MOUNTPOINTS "$TARGET" | grep -q '[^[:space:]]'; then + echo "Target or one of its partitions is mounted: $TARGET" >&2 + lsblk -o NAME,PATH,SIZE,MODEL,SERIAL,TYPE,MOUNTPOINTS "$TARGET" >&2 + exit 1 +fi +if command -v swapon >/dev/null && swapon --noheadings --raw --show=NAME \ + | grep -Eq "^${TARGET}(p[0-9]+)?$"; then + echo "Target contains active swap: $TARGET" >&2 + exit 1 +fi + +xz --test "$IMAGE" +if [[ -f "$IMAGE.sha256" ]]; then + (cd "$(dirname -- "$IMAGE")" && sha256sum --check --status "$(basename -- "$IMAGE.sha256")") || { + echo 'Image checksum failed' >&2 + exit 1 + } +fi +image_bytes=$(xz --robot --list "$IMAGE" | awk -F '\t' '$1 == "file" { print $5 }') +target_bytes=$(blockdev --getsize64 "$TARGET") +[[ "$image_bytes" =~ ^[0-9]+$ && "$target_bytes" -ge "$image_bytes" ]] || { + echo "Target is too small ($target_bytes bytes; image is $image_bytes bytes)" >&2 + exit 1 +} + +echo 'About to overwrite this entire device:' >&2 +lsblk -d -o NAME,PATH,SIZE,MODEL,SERIAL,TRAN,TYPE "$TARGET" >&2 +echo "Writing verified image: $IMAGE" >&2 +xz --decompress --stdout "$IMAGE" \ + | dd of="$TARGET" bs=16M iflag=fullblock oflag=direct conv=fsync status=progress +sync + +echo 'Reading the written region back for byte verification...' >&2 +cmp --silent --bytes="$image_bytes" <(xz --decompress --stdout "$IMAGE") "$TARGET" || { + echo 'Post-write verification FAILED; do not boot this device' >&2 + exit 1 +} +blockdev --rereadpt "$TARGET" 2>/dev/null || true +echo "Flash and read-back verification passed: $TARGET" >&2 diff --git a/scripts/live-migrate-root-to-nvme.sh b/scripts/live-migrate-root-to-nvme.sh new file mode 100755 index 0000000..fcdd532 --- /dev/null +++ b/scripts/live-migrate-root-to-nvme.sh @@ -0,0 +1,335 @@ +#!/usr/bin/env bash +# Move a running RK1 root filesystem from eMMC to a specifically identified +# NVMe while retaining U-Boot and /boot on eMMC. + +set -Eeuo pipefail +umask 022 + +TARGET="" +EXPECTED_SERIAL="" +BOOT_ENV=/boot/armbianEnv.txt +BOOT_ENV_BACKUP=/boot/armbianEnv.txt.pre-nvme +INSTALL_LOG=/var/log/armbian-install.log +INSTALL_LOG_BACKUP=/var/log/armbian-install.log.pre-nvme + +usage() { + cat <<'EOF' +Usage: sudo live-migrate-root-to-nvme.sh --target /dev/nvmeXnY --serial SERIAL + +The target NVMe is erased. The running root must be on eMMC. U-Boot and /boot +remain on eMMC; only the root filesystem is moved to NVMe. +EOF +} + +while (($#)); do + case "$1" in + --target) TARGET=${2:?}; shift 2 ;; + --serial) EXPECTED_SERIAL=${2:?}; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown argument: $1" >&2; usage >&2; exit 2 ;; + esac +done + +[[ $EUID -eq 0 ]] || { echo "Run this script through sudo" >&2; exit 2; } +[[ -n "$TARGET" && -n "$EXPECTED_SERIAL" ]] || { usage >&2; exit 2; } + +for command_name in armbian-install blkid chmod cmp cp debugfs dpkg dpkg-query \ + e2fsck findmnt flock grep lsof lsblk ps readlink sed swapon sync systemctl \ + udevadm umount xargs; do + command -v "$command_name" >/dev/null || { + echo "Missing required command: $command_name" >&2 + exit 1 + } +done + +exec 9>/run/lock/rk1-nvme-migrate.lock +flock -n 9 || { echo "Another RK1 NVMe migration is already running" >&2; exit 1; } + +TARGET=$(readlink -f -- "$TARGET") +[[ "$TARGET" =~ ^/dev/nvme[0-9]+n[0-9]+$ && -b "$TARGET" ]] || { + echo "Refusing non-NVMe whole-disk target: $TARGET" >&2 + exit 1 +} +[[ "$(lsblk -dnro TYPE "$TARGET")" == disk ]] || { + echo "Target is not a whole disk: $TARGET" >&2 + exit 1 +} +[[ "$(lsblk -dnro TRAN "$TARGET")" == nvme ]] || { + echo "Target transport is not NVMe: $TARGET" >&2 + exit 1 +} + +actual_serial=$(lsblk -dnro SERIAL "$TARGET" | xargs) +actual_model=$(lsblk -dnro MODEL "$TARGET" | xargs) +[[ "$actual_serial" == "$EXPECTED_SERIAL" ]] || { + echo "Target serial mismatch: expected '$EXPECTED_SERIAL', found '$actual_serial'" >&2 + exit 1 +} + +root_source=$(findmnt -no SOURCE --nofsroot /) +[[ "$root_source" =~ ^/dev/mmcblk[0-9]+p[0-9]+$ ]] || { + echo "Running root is not on an eMMC partition: $root_source" >&2 + exit 1 +} +root_disk="/dev/$(lsblk -no PKNAME "$root_source" | sed -n '1p')" +[[ -b "$root_disk" && "$root_disk" != "$TARGET" ]] || { + echo "Could not safely distinguish root disk from target" >&2 + exit 1 +} + +target_mounts=$(lsblk -nrpo MOUNTPOINTS "$TARGET" | sed '/^[[:space:]]*$/d') +[[ -z "$target_mounts" ]] || { + echo "Target or one of its partitions is mounted:" >&2 + printf '%s\n' "$target_mounts" >&2 + exit 1 +} +while IFS= read -r swap_name; do + [[ -z "$swap_name" ]] && continue + if [[ "$swap_name" == "$TARGET" || "$swap_name" == "${TARGET}p"* ]]; then + echo "Target contains active swap: $swap_name" >&2 + exit 1 + fi +done < <(swapon --noheadings --raw --show=NAME) + +[[ -e /var/lib/rk1-media/provisioned ]] || { + echo "RK1 media provisioning has not completed" >&2 + exit 1 +} +[[ "$(dpkg-query -W -f='${Status}' rk1-media-stack 2>/dev/null)" == \ + "install ok installed" ]] || { + echo "rk1-media-stack is not fully installed" >&2 + exit 1 +} +dpkg_audit=$(dpkg --audit) +[[ -z "$dpkg_audit" ]] || { + echo "dpkg reports an incomplete transaction:" >&2 + printf '%s\n' "$dpkg_audit" >&2 + exit 1 +} + +emmc_uuid=$(blkid -s UUID -o value "$root_source") +[[ -n "$emmc_uuid" ]] || { echo "Could not resolve eMMC UUID" >&2; exit 1; } +grep -Fxq "rootdev=UUID=$emmc_uuid" "$BOOT_ENV" || { + echo "$BOOT_ENV does not currently point at the running eMMC root" >&2 + exit 1 +} + +if [[ -e "$BOOT_ENV_BACKUP" ]]; then + cmp --silent "$BOOT_ENV" "$BOOT_ENV_BACKUP" || { + echo "Existing boot-environment backup differs; refusing to overwrite it" >&2 + exit 1 + } +else + cp -a -- "$BOOT_ENV" "$BOOT_ENV_BACKUP" +fi + +for apt_unit in apt-daily.service apt-daily-upgrade.service; do + apt_state=$(systemctl is-active "$apt_unit" 2>/dev/null || true) + case "$apt_state" in + inactive|failed|unknown) ;; + *) echo "Refusing migration while $apt_unit is $apt_state" >&2; exit 1 ;; + esac +done + +provision_state=$(systemctl is-active rk1-media-provision.service 2>/dev/null || true) +case "$provision_state" in + inactive|failed|unknown|active) ;; + *) echo "Provisioning is not quiescent: $provision_state" >&2; exit 1 ;; +esac + +echo "Confirmed source root: $root_source (UUID=$emmc_uuid)" +echo "ERASING target: $TARGET | $actual_model | serial $actual_serial" + +apt_daily_timer_was_active=0 +apt_upgrade_timer_was_active=0 +unattended_was_active=0 +kodi_was_active=0 +systemctl is-active --quiet apt-daily.timer && apt_daily_timer_was_active=1 +systemctl is-active --quiet apt-daily-upgrade.timer && apt_upgrade_timer_was_active=1 +systemctl is-active --quiet unattended-upgrades.service && unattended_was_active=1 +systemctl is-active --quiet kodi-rk.service && kodi_was_active=1 + +restore_services=0 +restore_boot_env=0 +cleanup() { + local rc=$? + trap - EXIT + if ((restore_boot_env)); then + echo "Migration did not validate; restoring the eMMC boot environment" >&2 + if ! cp -a -- "$BOOT_ENV_BACKUP" "$BOOT_ENV" || \ + ! cmp --silent "$BOOT_ENV_BACKUP" "$BOOT_ENV"; then + echo "CRITICAL: could not restore and verify $BOOT_ENV" >&2 + rc=1 + fi + if ! sync; then + echo "CRITICAL: writeback failed while restoring $BOOT_ENV" >&2 + rc=1 + fi + fi + if ((restore_services)); then + ((apt_daily_timer_was_active)) && systemctl start apt-daily.timer 2>/dev/null || true + ((apt_upgrade_timer_was_active)) && systemctl start apt-daily-upgrade.timer 2>/dev/null || true + ((unattended_was_active)) && systemctl start unattended-upgrades.service 2>/dev/null || true + ((kodi_was_active)) && systemctl start kodi-rk.service 2>/dev/null || true + fi + exit "$rc" +} +trap cleanup EXIT + +restore_services=1 +systemctl stop apt-daily.timer apt-daily-upgrade.timer 2>/dev/null || true +systemctl stop apt-daily.service apt-daily-upgrade.service 2>/dev/null || true +for apt_unit in apt-daily.service apt-daily-upgrade.service; do + apt_state=$(systemctl is-active "$apt_unit" 2>/dev/null || true) + case "$apt_state" in + inactive|failed|unknown) ;; + *) echo "Package service did not stop cleanly: $apt_unit is $apt_state" >&2; exit 1 ;; + esac +done +systemctl stop rk1-media-provision.service +! systemctl is-active --quiet rk1-media-provision.service || { + echo "Provisioning service did not stop cleanly" >&2 + exit 1 +} +systemctl stop unattended-upgrades.service 2>/dev/null || true +if systemctl list-unit-files kodi-rk.service --no-legend 2>/dev/null | grep -q '^kodi-rk\.service'; then + systemctl stop kodi-rk.service + ! systemctl is-active --quiet kodi-rk.service || { + echo "Kodi did not stop cleanly" >&2 + exit 1 + } +fi + +package_processes=$(ps -eo comm= | grep -E \ + '^(apt|apt-get|dpkg|unattended-upgr)$' || true) +[[ -z "$package_processes" ]] || { + echo "Package-management process remains active:" >&2 + printf '%s\n' "$package_processes" >&2 + exit 1 +} +lock_holders=$(lsof -t /var/lib/dpkg/lock /var/lib/dpkg/lock-frontend \ + /var/cache/apt/archives/lock 2>/dev/null || true) +[[ -z "$lock_holders" ]] || { + echo "A package-management lock is still held by PID(s): $lock_holders" >&2 + exit 1 +} +dpkg_audit=$(dpkg --audit) +[[ -z "$dpkg_audit" ]] || { + echo "dpkg became inconsistent before migration:" >&2 + printf '%s\n' "$dpkg_audit" >&2 + exit 1 +} + +if [[ -s "$INSTALL_LOG" ]]; then + [[ ! -e "$INSTALL_LOG_BACKUP" ]] || { + echo "Installer log backup already exists: $INSTALL_LOG_BACKUP" >&2 + exit 1 + } + cp -a -- "$INSTALL_LOG" "$INSTALL_LOG_BACKUP" +fi +: >"$INSTALL_LOG" +chmod 0600 "$INSTALL_LOG" + +sync +restore_boot_env=1 +armbian-install --target "$TARGET" --boot sd --fs ext4 --yes + +target_partition="${TARGET}p1" +udevadm settle +[[ -b "$target_partition" ]] || { + echo "Installer did not create $target_partition" >&2 + exit 1 +} + +mapfile -t post_install_mounts < <(findmnt -rn -S "$target_partition" -o TARGET) +for mountpoint in "${post_install_mounts[@]}"; do + [[ "$mountpoint" == /mnt/armbian-install.* ]] || { + echo "Target remains mounted at unexpected path: $mountpoint" >&2 + exit 1 + } + umount "$mountpoint" || { + echo "Could not unmount installer path: $mountpoint" >&2 + exit 1 + } +done +findmnt -rn -S "$target_partition" >/dev/null && { + echo "Target partition remains mounted after installer teardown" >&2 + exit 1 +} + +grep -Fq "scenario: sd install to $TARGET completed" "$INSTALL_LOG" || { + echo "Installer did not record successful sd-mode completion" >&2 + exit 1 +} + +nvme_uuid=$(blkid -s UUID -o value "$target_partition") +[[ -n "$nvme_uuid" && "$nvme_uuid" != "$emmc_uuid" ]] || { + echo "NVMe UUID is missing or duplicates the eMMC UUID" >&2 + exit 1 +} +mapfile -t uuid_devices < <(blkid -t "UUID=$nvme_uuid" -o device) +[[ ${#uuid_devices[@]} -eq 1 && "${uuid_devices[0]}" == "$target_partition" ]] || { + echo "NVMe filesystem UUID is not unique" >&2 + printf '%s\n' "${uuid_devices[@]}" >&2 + exit 1 +} +grep -Fxq "rootdev=UUID=$nvme_uuid" "$BOOT_ENV" || { + echo "eMMC boot environment does not point to the new NVMe root" >&2 + exit 1 +} +grep -Fxq 'rootfstype=ext4' "$BOOT_ENV" || { + echo "eMMC boot environment does not select ext4 for the new root" >&2 + exit 1 +} + +e2fsck -fn "$target_partition" +target_fstab=$(debugfs -R 'cat /etc/fstab' "$target_partition" 2>/dev/null) +root_mount_count=$(grep -Ec '^[^#[:space:]][^[:space:]]*[[:space:]]+/[[:space:]]+' \ + <<<"$target_fstab" || true) +[[ "$root_mount_count" -eq 1 ]] || { + echo "Target fstab does not contain exactly one root mount" >&2 + exit 1 +} +grep -Eq "^UUID=${nvme_uuid}[[:space:]]+/[[:space:]]+ext4" <<<"$target_fstab" || { + echo "Target fstab does not mount the NVMe UUID at /" >&2 + exit 1 +} +grep -Eq "^UUID=${emmc_uuid}[[:space:]]+/media/boot-media[[:space:]]+ext4" \ + <<<"$target_fstab" || { + echo "Target fstab does not mount the eMMC boot medium" >&2 + exit 1 +} +grep -Eq '^/media/boot-media/boot[[:space:]]+/boot[[:space:]]+none[[:space:]]+bind' \ + <<<"$target_fstab" || { + echo "Target fstab does not bind the eMMC /boot directory" >&2 + exit 1 +} + +while read -r fstab_source _; do + case "$fstab_source" in + UUID=*) + fstab_uuid=${fstab_source#UUID=} + mapfile -t fstab_devices < <(blkid -t "UUID=$fstab_uuid" -o device) + [[ ${#fstab_devices[@]} -eq 1 ]] || { + echo "Target fstab UUID does not resolve uniquely: $fstab_uuid" >&2 + exit 1 + } + ;; + PARTUUID=*) + fstab_partuuid=${fstab_source#PARTUUID=} + mapfile -t fstab_devices < <(blkid -t "PARTUUID=$fstab_partuuid" -o device) + [[ ${#fstab_devices[@]} -eq 1 ]] || { + echo "Target fstab PARTUUID does not resolve uniquely: $fstab_partuuid" >&2 + exit 1 + } + ;; + esac +done <<<"$target_fstab" + +sync +restore_boot_env=0 +restore_services=0 +echo "MIGRATION_VALIDATED" +echo "NVMe root: $target_partition (UUID=$nvme_uuid)" +echo "eMMC boot fallback: $BOOT_ENV_BACKUP" +echo "REBOOT_REQUIRED" diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..1fd5ce6 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,19 @@ +# Validation + +Run all host-side checks, the offline package-transaction simulation, and the +real acquisition-only APT regression (no packages are installed): + +```bash +./tests/run-static.sh +``` + +Validate a finished image without mounting it or using loop devices: + +```bash +./tests/validate-image.sh dist/rk1-media-20260817-r4-trixie-vendor-6.1.115-turing-rk1-emmc.img +``` + +Passing these checks establishes image structure, input integrity, package +closure, boot payload preservation, and security invariants. HDMI link, +hardware codecs, RGA, GPU, audio/CEC, and RKNN execution still require the +physical acceptance test after flashing. diff --git a/tests/run-static.sh b/tests/run-static.sh new file mode 100755 index 0000000..208335f --- /dev/null +++ b/tests/run-static.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) +PROJECT_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd -P) + +while IFS= read -r -d '' script; do + bash -n "$script" +done < <(find "$PROJECT_DIR" -path '*/sources/*' -prune -o -type f -name '*.sh' -print0) +"$PROJECT_DIR/armbian/tests/lint.sh" +"$SCRIPT_DIR/test-firstboot-ssh.sh" +RKNN_SOURCE_DIR="$PROJECT_DIR/downloads/rknn-toolkit2-v2.3.2-minimal" \ +"$PROJECT_DIR/runtime/tests/run-tests.sh" +"$SCRIPT_DIR/test-media-package.sh" +bash "$SCRIPT_DIR/test-provisioning-recipe.sh" +"$SCRIPT_DIR/test-offline-apt.sh" +echo 'PASS: all static/integration tests' diff --git a/tests/test-firstboot-ssh.sh b/tests/test-firstboot-ssh.sh new file mode 100755 index 0000000..d88b5c4 --- /dev/null +++ b/tests/test-firstboot-ssh.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) +PROJECT_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd -P) +IDENTITY="$PROJECT_DIR/image/overlay/usr/local/sbin/rk1-media-identity" +UNIT="$PROJECT_DIR/image/overlay/etc/systemd/system/rk1-media-identity.service" +DROPIN="$PROJECT_DIR/image/overlay/etc/systemd/system/ssh.service.d/10-rk1-media-identity.conf" + +runtime_line=$(grep -nF 'install -d -o root -g root -m 0755 /run/sshd' "$IDENTITY" | cut -d: -f1) +validation_line=$(grep -nF '/usr/sbin/sshd -t' "$IDENTITY" | cut -d: -f1) +[[ -n "$runtime_line" && -n "$validation_line" && "$runtime_line" -lt "$validation_line" ]] || { + echo 'SSH runtime directory is not created before configuration validation' >&2 + exit 1 +} +grep -qx 'RuntimeDirectory=sshd' "$UNIT" +grep -qx 'RuntimeDirectoryMode=0755' "$UNIT" +grep -qx 'Requires=rk1-media-identity.service' "$DROPIN" +grep -qx 'After=rk1-media-identity.service' "$DROPIN" + +echo 'PASS: first-boot SSH runtime ordering regression' diff --git a/tests/test-media-package.sh b/tests/test-media-package.sh new file mode 100755 index 0000000..ffc4416 --- /dev/null +++ b/tests/test-media-package.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) +PROJECT_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd -P) +DEB=${1:-"$PROJECT_DIR/media/out/rk1-media-stack_20260721.1741+rk1.1_arm64.deb"} +[[ -f "$DEB" ]] || { echo "Missing media package: $DEB" >&2; exit 1; } + +TEST_TMP=$(mktemp -d /tmp/rk1-media-package-test.XXXXXXXX) +trap 'rm -rf -- "$TEST_TMP"' EXIT +dpkg-deb --info "$DEB" >/dev/null +dpkg-deb --extract "$DEB" "$TEST_TMP/root" +dpkg-deb --control "$DEB" "$TEST_TMP/control" + +[[ "$(dpkg-deb --field "$DEB" Package)" == rk1-media-stack ]] +[[ "$(dpkg-deb --field "$DEB" Architecture)" == arm64 ]] +if find "$TEST_TMP/root" \( -type d -o -type f \) -perm /0022 -print -quit | grep -q .; then + echo 'Package contains a group/world-writable path' >&2 + exit 1 +fi +! find "$TEST_TMP/root" -type f -name '*.a' -print -quit | grep -q . +! find "$TEST_TMP/root" -type d -name pkgconfig -print -quit | grep -q . +[[ -x "$TEST_TMP/root/opt/rkmedia/bin/ffmpeg-rk" ]] +[[ -x "$TEST_TMP/root/opt/rkmedia/bin/kodi-rk" ]] +[[ -f "$TEST_TMP/root/usr/lib/systemd/system/kodi-rk.service" ]] +[[ ! -e "$TEST_TMP/root/usr/lib/systemd/system/kodi.service" ]] +grep -q 'KERNEL=="rknpu\*"' "$TEST_TMP/root/usr/lib/udev/rules.d/70-rkmedia.rules" +grep -q 'udevadm trigger.*subsystem-match=drm' "$TEST_TMP/control/postinst" +! grep -q '^ConditionPathExists=/dev/dri/card0' \ + "$TEST_TMP/root/usr/lib/systemd/system/kodi-rk.service" + +echo "PASS: media package structure ($DEB)" diff --git a/tests/test-offline-apt.sh b/tests/test-offline-apt.sh new file mode 100755 index 0000000..4d3d2f2 --- /dev/null +++ b/tests/test-offline-apt.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) +PROJECT_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd -P) +DEPENDENCY_ARCHIVES="$PROJECT_DIR/work/apt/archives" +MEDIA_DEB="$PROJECT_DIR/media/out/rk1-media-stack_20260721.1741+rk1.1_arm64.deb" +[[ -d "$DEPENDENCY_ARCHIVES" ]] || { echo "Missing APT archives: $DEPENDENCY_ARCHIVES" >&2; exit 1; } +[[ -f "$MEDIA_DEB" ]] || { echo "Missing media package: $MEDIA_DEB" >&2; exit 1; } + +TEST_TMP=$(mktemp -d "$PROJECT_DIR/work/rk1-offline-apt.XXXXXXXX") +trap 'rm -rf -- "$TEST_TMP"' EXIT +SEED_ARCHIVES="$TEST_TMP/opt/rk1-seed/debs" +mkdir -p "$SEED_ARCHIVES/partial" "$TEST_TMP/state" + +# Hard links keep this acquisition regression test fast while presenting APT +# with the exact single-directory cache layout used in the finished image. +while IFS= read -r -d '' source_deb; do + ln -- "$source_deb" "$SEED_ARCHIVES/$(basename -- "$source_deb")" +done < <(find "$DEPENDENCY_ARCHIVES" -maxdepth 1 -type f -name '*.deb' -print0 | sort -z) +ln -- "$MEDIA_DEB" "$SEED_ARCHIVES/$(basename -- "$MEDIA_DEB")" +mapfile -d '' debs < <(find "$SEED_ARCHIVES" -maxdepth 1 -type f -name '*.deb' -print0 | sort -z) +[[ "${#debs[@]}" == 141 ]] + +apt_options=( + -o APT::Architecture=arm64 + -o Dir::State::status="$PROJECT_DIR/work/base-meta/status" + -o Dir::State::lists="$PROJECT_DIR/work/apt/lists" + -o Dir::State::extended_states="$TEST_TMP/state/extended_states" + -o Dir::Cache::archives="$SEED_ARCHIVES" + -o Dir::Cache::pkgcache="$TEST_TMP/pkgcache.bin" + -o Dir::Cache::srcpkgcache="$TEST_TMP/srcpkgcache.bin" + -o Dir::Etc::sourcelist="$PROJECT_DIR/work/base-meta/debian-build.sources" + -o Dir::Etc::sourceparts=- + -o Debug::NoLocking=true + -o APT::Sandbox::User= + -o APT::Keep-Downloaded-Packages=true +) + +simulation_log="$TEST_TMP/simulation.log" +apt-get "${apt_options[@]}" --simulate --no-download --no-install-recommends \ + install "${debs[@]}" >"$simulation_log" +grep -q '^0 upgraded, 141 newly installed, 0 to remove' "$simulation_log" +[[ "$(grep -c '^Inst ' "$simulation_log")" == 141 ]] +[[ "$(grep -c '^Conf ' "$simulation_log")" == 141 ]] + +( + cd "$SEED_ARCHIVES" + sha256sum -- *.deb >"$TEST_TMP/debs.before.sha256" +) +acquisition_log="$TEST_TMP/acquisition.log" +apt-get "${apt_options[@]}" --download-only --no-download \ + --no-install-recommends --yes install "${debs[@]}" >"$acquisition_log" +grep -q '^0 upgraded, 141 newly installed, 0 to remove' "$acquisition_log" +grep -Eq '^Download complete( and in download only mode)?$' "$acquisition_log" +! grep -q '^Need to get ' "$acquisition_log" +( + cd "$SEED_ARCHIVES" + sha256sum -- *.deb >"$TEST_TMP/debs.after.sha256" +) +cmp --silent "$TEST_TMP/debs.before.sha256" "$TEST_TMP/debs.after.sha256" + +echo 'PASS: offline APT closure and real acquisition (141 packages, zero downloads)' diff --git a/tests/test-provisioning-recipe.sh b/tests/test-provisioning-recipe.sh new file mode 100644 index 0000000..ee1ad45 --- /dev/null +++ b/tests/test-provisioning-recipe.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) +PROJECT_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd -P) +PROVISION="$PROJECT_DIR/image/overlay/usr/local/sbin/rk1-media-provision" +UNIT="$PROJECT_DIR/image/overlay/etc/systemd/system/rk1-media-provision.service" + +grep -Fq 'seed_archives=/opt/rk1-seed/debs' "$PROVISION" +grep -Fq 'install -d -o _apt -g root -m 0700 "$seed_archives/partial"' "$PROVISION" +grep -Fq -- '-o Dir::Cache::archives="$seed_archives"' "$PROVISION" +grep -Fq -- '--no-download --no-install-recommends --yes install' "$PROVISION" + +grep -Eq '^Before=.*kodi-rk\.service$' "$UNIT" +grep -Eq '^systemctl --no-block start .*kodi-rk\.service$' "$PROVISION" +! grep -Eq '^systemctl start .*kodi-rk\.service' "$PROVISION" + +for subsystem in drm dma_heap misc; do + grep -Fq "udevadm trigger --action=add --subsystem-match=$subsystem" "$PROVISION" +done +! grep -Eq '^udevadm trigger --action=change' "$PROVISION" + +echo 'PASS: provisioning cache, unit ordering, and device-event recipe' diff --git a/tests/validate-image.sh b/tests/validate-image.sh new file mode 100755 index 0000000..4e5875e --- /dev/null +++ b/tests/validate-image.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +# Read-only, loopless validation of a composed Turing RK1 image. + +set -Eeuo pipefail + +SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) +PROJECT_DIR=$(cd -- "$SCRIPT_DIR/.." && pwd -P) +IMAGE=${1:-} +BASE_RAW=${2:-"$PROJECT_DIR/downloads/Armbian_community_26.11.0-trunk.1_Turing-rk1_trixie_vendor_6.1.115_minimal.img"} +[[ -n "$IMAGE" && -f "$IMAGE" ]] || { + echo "Usage: $0 IMAGE.img[.xz] [BASE.img]" >&2 + exit 2 +} + +for tool in cmp debugfs e2fsck jq sfdisk sgdisk sha256sum xz; do + command -v "$tool" >/dev/null || { echo "Missing command: $tool" >&2; exit 1; } +done + +TEST_TMP=$(mktemp -d /tmp/rk1-image-validation.XXXXXXXX) +trap 'rm -rf -- "$TEST_TMP"' EXIT +RAW_IMAGE=$IMAGE +if [[ "$IMAGE" == *.xz ]]; then + xz --test "$IMAGE" + RAW_IMAGE="$TEST_TMP/image.img" + xz --decompress --stdout "$IMAGE" >"$RAW_IMAGE" +fi + +sgdisk --verify "$RAW_IMAGE" >/dev/null +partition_json=$(sfdisk --json "$RAW_IMAGE") +[[ "$(jq '.partitiontable.partitions | length' <<<"$partition_json")" == 1 ]] || { + echo 'Image must contain exactly one root partition' >&2 + exit 1 +} +partition_start=$(jq -r '.partitiontable.partitions[0].start' <<<"$partition_json") +partition_size=$(jq -r '.partitiontable.partitions[0].size' <<<"$partition_json") +partition_type=$(jq -r '.partitiontable.partitions[0].type' <<<"$partition_json") +[[ "$partition_start" == 32768 ]] || { echo "Unexpected partition start: $partition_start" >&2; exit 1; } +[[ "$partition_type" == B921B045-1DF0-41C3-AF44-4C6F280D3FAE ]] || { + echo "Unexpected ARM64 root partition type: $partition_type" >&2 + exit 1 +} + +if [[ -f "$BASE_RAW" ]]; then + cmp --silent --bytes=440 "$BASE_RAW" "$RAW_IMAGE" || { + echo 'MBR bootstrap differs from the locked base image' >&2 + exit 1 + } + loader_offset=$((64 * 512)) + loader_length=$(((32768 - 64) * 512)) + cmp --silent --ignore-initial="$loader_offset:$loader_offset" \ + --bytes="$loader_length" "$BASE_RAW" "$RAW_IMAGE" || { + echo 'Rockchip loader region differs from the locked base image' >&2 + exit 1 + } +fi + +ROOTFS="$TEST_TMP/rootfs.ext4" +dd if="$RAW_IMAGE" of="$ROOTFS" bs=512 skip="$partition_start" \ + count="$partition_size" status=none +e2fsck -fn "$ROOTFS" >"$TEST_TMP/e2fsck.log" 2>&1 || { + cat "$TEST_TMP/e2fsck.log" >&2 + exit 1 +} + +ext4_stat() { debugfs -R "stat $1" "$ROOTFS" 2>/dev/null; } +ext4_exists() { ext4_stat "$1" | grep -q '^Inode:'; } +assert_contains() { + local path=$1 pattern=$2 + debugfs -R "cat $path" "$ROOTFS" 2>/dev/null | grep -Eq "$pattern" || { + echo "Missing $pattern in $path" >&2 + exit 1 + } +} +assert_mode() { + local path=$1 mode=$2 + ext4_stat "$path" | grep -Eq "Mode:[[:space:]]+$mode" || { + echo "Unexpected mode for $path (wanted $mode)" >&2 + exit 1 + } +} + +assert_contains /etc/os-release '^VERSION_ID="?13"?$' +assert_contains /etc/armbian-release '^BOARD=turing-rk1$' +assert_contains /boot/armbianEnv.txt '^fdtfile=rockchip/rk3588-turing-rk1\.dtb$' +assert_contains /boot/armbianEnv.txt '^overlays=.*panthor-gpu' +assert_contains /etc/shadow '^root:!:' +assert_contains /etc/default/armbian-firstrun '^OPENSSHD_REGENERATE_HOST_KEYS=false$' +assert_contains /etc/ssh/sshd_config.d/90-rk1-media.conf '^PermitRootLogin no$' +assert_contains /etc/ssh/sshd_config.d/90-rk1-media.conf '^PasswordAuthentication no$' +assert_contains /etc/ssh/sshd_config.d/90-rk1-media.conf '^AuthenticationMethods publickey$' +assert_contains /etc/systemd/system/rk1-media-provision.service '^Before=.*kodi-rk\.service$' +assert_contains /usr/local/sbin/rk1-media-provision \ + '^seed_archives=/opt/rk1-seed/debs$' +assert_contains /usr/local/sbin/rk1-media-provision \ + 'Dir::Cache::archives=.*seed_archives' +assert_contains /usr/local/sbin/rk1-media-provision \ + '^systemctl --no-block start .*kodi-rk\.service$' +assert_contains /usr/local/sbin/rk1-media-provision \ + '^udevadm trigger --action=add --subsystem-match=misc' +assert_contains /etc/systemd/system/rk1-media-identity.service '^RuntimeDirectory=sshd$' +assert_contains /usr/local/sbin/rk1-media-identity \ + '^install -d -o root -g root -m 0755 /run/sshd$' +assert_contains /usr/local/sbin/rk1-media-identity '^/usr/sbin/sshd -t$' +assert_contains /opt/rk1-seed/debs.sha256 \ + '^dd0ca68696aa35b1998f9c9e131fdf8f0d1ef4ba6f58a36d0d99f0625d47f430 rk1-media-stack_' + +for required_path in \ + /boot/dtb/rockchip/rk3588-turing-rk1.dtb \ + /boot/dtb/rockchip/overlay/rockchip-rk3588-panthor-gpu.dtbo \ + /lib/modules/6.1.115-vendor-rk35xx/kernel/drivers/gpu/drm/panthor/panthor.ko \ + /opt/rknn/2.3.2/lib/librknnrt.so \ + /opt/rknn/2.3.2/share/models/rk3588/mobilenet_v1.rknn \ + /usr/local/bin/rknn-inference-test \ + /usr/local/bin/rk1-media-selftest \ + /usr/local/sbin/rk1-media-migrate-root-to-nvme \ + /usr/local/sbin/rk1-media-diagnostics; do + ext4_exists "$required_path" || { echo "Missing image payload: $required_path" >&2; exit 1; } +done + +for critical_dir in / /etc /etc/ssh /etc/systemd /usr /opt; do + assert_mode "$critical_dir" 0755 +done +assert_mode /etc/shadow 0640 +ext4_stat /etc/shadow | grep -Eq 'Group:[[:space:]]+42' || { + echo '/etc/shadow is not owned by the shadow group' >&2 + exit 1 +} +assert_mode /usr/local/sbin/rk1-media-identity 0755 +assert_mode /usr/local/sbin/rk1-media-provision 0755 +assert_mode /usr/local/sbin/rk1-media-migrate-root-to-nvme 0755 + +ext4_stat /etc/systemd/system/sysinit.target.wants/rk1-media-identity.service \ + | grep -Fq 'Fast link dest: "../rk1-media-identity.service"' || exit 1 +ext4_stat /etc/systemd/system/multi-user.target.wants/rk1-media-provision.service \ + | grep -Fq 'Fast link dest: "../rk1-media-provision.service"' || exit 1 + +for forbidden_path in /root/.not_logged_in_yet /root/.ssh/authorized_keys \ + /etc/ssh/ssh_host_rsa_key /etc/ssh/ssh_host_ecdsa_key \ + /etc/ssh/ssh_host_ed25519_key; do + if ext4_exists "$forbidden_path"; then + echo "Image contains forbidden credential: $forbidden_path" >&2 + exit 1 + fi +done + +deb_count=$(debugfs -R 'ls -p /opt/rk1-seed/debs' "$ROOTFS" 2>/dev/null \ + | grep -c '/.*\.deb/' || true) +[[ "$deb_count" == 141 ]] || { echo "Expected 141 offline packages, found $deb_count" >&2; exit 1; } + +echo "PASS: loopless RK1 image validation ($IMAGE)"