Initial RK1 media-center image project

This commit is contained in:
2026-08-17 18:42:29 +00:00
commit 5fe41e79e9
81 changed files with 5725 additions and 0 deletions
+97
View File
@@ -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"
+87
View File
@@ -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
+335
View File
@@ -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"