Initial RK1 media-center image project
This commit is contained in:
Executable
+325
@@ -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)|[email protected]|[email protected])$/ { 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"
|
||||
Executable
+61
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
APT::Periodic::Update-Package-Lists "1";
|
||||
APT::Periodic::Unattended-Upgrade "1";
|
||||
@@ -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";
|
||||
@@ -0,0 +1 @@
|
||||
rk1-media
|
||||
@@ -0,0 +1,7 @@
|
||||
PermitRootLogin no
|
||||
PasswordAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
PubkeyAuthentication yes
|
||||
AuthenticationMethods publickey
|
||||
PermitEmptyPasswords no
|
||||
AllowUsers rkadmin
|
||||
@@ -0,0 +1 @@
|
||||
../rk1-media-provision.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
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
[Unit]
|
||||
Requires=rk1-media-identity.service
|
||||
After=rk1-media-identity.service
|
||||
@@ -0,0 +1 @@
|
||||
../../rk1-media-identity.service
|
||||
+36
@@ -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
|
||||
+51
@@ -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
|
||||
Reference in New Issue
Block a user