Repository navigation
Expand file tree
/
Copy pathbuild.sh
More file actions
167 lines (157 loc) · 11.2 KB
/
Copy pathbuild.sh
File metadata and controls
167 lines (157 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#!/usr/bin/env bash
set -euo pipefail
usage(){ echo 'Usage: sudo ./build.sh <board-id> [--check-only] [--rootfs-dir PATH] [--image PATH]'; }
[[ $# -ge 1 ]] || { usage; exit 2; }
board_id=$1; shift; check_only=0; rootfs_dir=; image_path=
while (($#)); do case $1 in --check-only) check_only=1;; --rootfs-dir) shift; rootfs_dir=$1;; --image) shift; image_path=$1;; -h|--help) usage; exit 0;; *) echo "Unknown argument: $1" >&2; exit 2;; esac; shift; done
board_file="boards/${board_id}.conf"; [[ -f $board_file ]] || { echo "Unknown board: $board_id" >&2; exit 1; }; source "$board_file"
required_vars=(BOARD_ID BOARD_NAME ARCH DEBIAN_ARCH SOC_FAMILY BOOTLOADER DTB KERNEL_FAMILY SUPPORT_TIER)
for var in "${required_vars[@]}"; do [[ -n ${!var:-} ]] || { echo "Missing board metadata: $var" >&2; exit 1; }; done
required_cmds=(bash awk sed grep sha256sum); ((check_only)) || required_cmds+=(mmdebstrap truncate sfdisk losetup mkfs.ext4 mount umount rsync)
missing=(); for cmd in "${required_cmds[@]}"; do command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd"); done
(("${#missing[@]}"==0)) || { printf 'Missing required host commands: %s\n' "${missing[*]}" >&2; exit 1; }
printf 'LeanPi M1.2 builder\nBoard: %s\nArchitecture: %s\nSoC family: %s\nBootloader: %s\nDTB: %s\nKernel: %s\nSupport: %s\n' "$BOARD_NAME" "$DEBIAN_ARCH" "$SOC_FAMILY" "$BOOTLOADER" "$DTB" "$KERNEL_FAMILY" "$SUPPORT_TIER"
((check_only)) && { echo 'M1.2 preflight: PASS'; exit 0; }
[[ $EUID -eq 0 ]] || { echo 'LeanPi image build must run as root.' >&2; exit 1; }
release=${DEBIAN_RELEASE:-${BASE_RELEASE:-trixie}}; mirror=${DEBIAN_MIRROR:-https://deb.debian.org/debian}; out_dir="out/${BOARD_ID}"
rootfs_dir=${rootfs_dir:-"${out_dir}/rootfs"}; image_path=${image_path:-"${out_dir}/leanpi-${BOARD_ID}.img"}; image_size_mb=${IMAGE_SIZE_MB:-768}; mkdir -p "$out_dir"
if [[ -e $rootfs_dir ]]; then [[ -d $rootfs_dir && -z $(find "$rootfs_dir" -mindepth 1 -maxdepth 1 -print -quit) ]] || { echo "Refusing non-empty path: $rootfs_dir" >&2; exit 1; }; else mkdir -p "$rootfs_dir"; fi
echo "Bootstrapping Debian $release ($DEBIAN_ARCH)"
mmdebstrap --architectures="$DEBIAN_ARCH" --variant=minbase --components=main --include=systemd-sysv,ca-certificates,iproute2,ifupdown,isc-dhcp-client,dropbear,procps "$release" "$rootfs_dir" "$mirror"
mkdir -p "$rootfs_dir/etc/leanpi"
printf 'LEANPI_BOARD=%s\nLEANPI_BOARD_NAME="%s"\nLEANPI_DEBIAN_RELEASE=%s\nLEANPI_ARCH=%s\nLEANPI_SUPPORT_TIER=%s\n' "$BOARD_ID" "$BOARD_NAME" "$release" "$DEBIAN_ARCH" "$SUPPORT_TIER" > "$rootfs_dir/etc/leanpi/release"
printf 'leanpi\n' > "$rootfs_dir/etc/hostname"; printf 'auto lo\niface lo inet loopback\n\nallow-hotplug eth0\niface eth0 inet dhcp\n' > "$rootfs_dir/etc/network/interfaces"
# Emit the qualification marker as soon as systemd is running on the real
# root filesystem. Avoid basic.target: H3 device/udev settling is very slow in QEMU.
cat > "$rootfs_dir/etc/systemd/system/leanpi-boot-complete.service" <<'EOF'
[Unit]
Description=LeanPi qualification boot marker
DefaultDependencies=no
After=systemd-remount-fs.service
Before=systemd-udev-trigger.service multi-user.target
[Service]
Type=oneshot
ExecStart=/bin/sh -c 'touch /run/leanpi-boot-complete; printf "<6>LEANPI_BOOT_COMPLETE\\n" > /dev/kmsg 2>/dev/null || true'
EOF
cat > "$rootfs_dir/etc/systemd/system/leanpi-qualification-metrics.service" <<'EOF'
[Unit]
Description=LeanPi qualification resource metrics
DefaultDependencies=no
After=leanpi-boot-complete.service systemd-udev-trigger.service
Wants=leanpi-boot-complete.service systemd-udev-trigger.service
Before=multi-user.target
[Service]
Type=oneshot
ExecStart=/bin/sh -c 'if [ -x /usr/local/sbin/leanpi-resource-baseline ]; then /usr/local/sbin/leanpi-resource-baseline /run/leanpi-resource-baseline.txt > /dev/kmsg 2>&1 || true; fi; if [ -x /usr/local/sbin/leanpi-service-audit ]; then /usr/local/sbin/leanpi-service-audit /run/leanpi-service-audit.txt > /dev/kmsg 2>&1 || true; fi'
EOF
mkdir -p "$rootfs_dir/etc/systemd/system/sysinit.target.wants"
ln -s ../leanpi-boot-complete.service "$rootfs_dir/etc/systemd/system/sysinit.target.wants/leanpi-boot-complete.service"
ln -s ../leanpi-qualification-metrics.service "$rootfs_dir/etc/systemd/system/sysinit.target.wants/leanpi-qualification-metrics.service"
# Keep emulated qualification focused on headless/server boot. The generic
# armmp kernel otherwise spends minutes probing H3 multimedia devices that are
# irrelevant to LeanPi's serial CI gate.
if [[ "${SUPPORT_TIER:-}" == emulated && "${SOC_FAMILY:-}" == sun8i ]]; then
mkdir -p "$rootfs_dir/etc/modprobe.d"
cat > "$rootfs_dir/etc/modprobe.d/leanpi-qemu-headless.conf" <<'EOF'
# LeanPi QEMU qualification: skip nonessential H3 multimedia stacks.
blacklist lima
blacklist sunxi_cedrus
blacklist sun8i_drm_hdmi
blacklist sun8i_mixer
blacklist sun4i_tcon
blacklist sun4i_drm
blacklist sunxi_ir
EOF
fi
echo "Creating ${image_size_mb} MiB raw image: $image_path"; rm -f "$image_path"; truncate -s "${image_size_mb}M" "$image_path"; printf 'label: dos\nunit: sectors\n\nstart=8192, type=83, bootable\n' | sfdisk "$image_path" >/dev/null
# Install board-family boot payload before copying the completed rootfs.
if [[ $KERNEL_FAMILY == sunxi ]]; then bash scripts/install-sunxi-boot.sh "$BOARD_ID" "$rootfs_dir" "$image_path"; fi
install -Dm755 scripts/leanpi-config "$rootfs_dir/usr/local/sbin/leanpi-config"
mkdir -p "$rootfs_dir/etc/systemd/journald.conf.d"
cat > "$rootfs_dir/etc/systemd/journald.conf.d/leanpi.conf" <<'EOF'
[Journal]
Storage=volatile
RuntimeMaxUse=1M
RuntimeKeepFree=16M
MaxFileSec=5min
Compress=yes
Seal=no
ForwardToWall=no
EOF
# Storage is deliberately volatile, so there is no persistent journal to flush.
# The journal catalog is also already part of the built image; rebuilding it on
# every small-board boot only creates transient I/O and memory pressure.
ln -sf /dev/null "$rootfs_dir/etc/systemd/system/systemd-journal-flush.service"
ln -sf /dev/null "$rootfs_dir/etc/systemd/system/systemd-journal-catalog-update.service"
# Keep temporary writes off flash without allowing tmpfs to consume unbounded RAM.
mkdir -p "$rootfs_dir/etc/systemd/system/tmp.mount.d" "$rootfs_dir/etc/systemd/system/local-fs.target.wants"
cat > "$rootfs_dir/etc/systemd/system/tmp.mount.d/leanpi.conf" <<'EOF'
[Mount]
Options=mode=1777,strictatime,nosuid,nodev,size=32M,nr_inodes=16k
EOF
ln -sf ../tmp.mount "$rootfs_dir/etc/systemd/system/local-fs.target.wants/tmp.mount"
# LeanPi Base is headless-first. Keep the serial console for local recovery,
# but do not spend idle RAM/processes on virtual-console gettys. A tty getty can
# still be explicitly unmasked by profiles or users that need a local display.
mkdir -p "$rootfs_dir/etc/systemd/system"
for tty in tty1 tty2 tty3 tty4 tty5 tty6; do
# Mask the instantiated unit itself. Masking only the wants/ symlink is
# overwritten by Debian's getty generator during boot.
ln -sf /dev/null "$rootfs_dir/etc/systemd/system/getty@$tty.service"
done
# Keep /var/tmp persistent: applications may rely on data surviving reboot.
# Bound APT's package cache instead of spending scarce RAM on another tmpfs.
mkdir -p "$rootfs_dir/etc/apt/apt.conf.d"
cat > "$rootfs_dir/etc/apt/apt.conf.d/90leanpi-cache" <<'EOF'
APT::Keep-Downloaded-Packages "false";
Binary::apt::APT::Keep-Downloaded-Packages "false";
Acquire::Languages "none";
EOF
# Periodic package-list cleanup is intentionally not implemented as a daemon/timer.
# Explicit package operations remain predictable and background wakeups stay at zero.
# Headless small-board defaults: disable background facilities that provide no
# value in LeanPi Base. Masking is deterministic and users can explicitly unmask.
for unit in apt-daily.service apt-daily.timer apt-daily-upgrade.service apt-daily-upgrade.timer man-db.service man-db.timer e2scrub_all.service e2scrub_all.timer dpkg-db-backup.service dpkg-db-backup.timer fstrim.service fstrim.timer systemd-hostnamed.service systemd-hostnamed.socket systemd-sysext.service systemd-sysext.socket
do
ln -sf /dev/null "$rootfs_dir/etc/systemd/system/$unit"
done
install -Dm755 scripts/leanpi-firstboot "$rootfs_dir/usr/local/sbin/leanpi-firstboot"
install -Dm644 systemd/leanpi-firstboot.service "$rootfs_dir/etc/systemd/system/leanpi-firstboot.service"
mkdir -p "$rootfs_dir/etc/systemd/system/multi-user.target.wants" "$rootfs_dir/etc/leanpi/firstboot.d"
ln -sf ../leanpi-firstboot.service "$rootfs_dir/etc/systemd/system/multi-user.target.wants/leanpi-firstboot.service"
install -Dm755 scripts/service-audit.sh "$rootfs_dir/usr/local/sbin/leanpi-service-audit"
install -Dm755 scripts/resource-baseline.sh "$rootfs_dir/usr/local/sbin/leanpi-resource-baseline"
if [[ $KERNEL_FAMILY == virt ]]; then bash scripts/install-virt-boot.sh "$BOARD_ID" "$rootfs_dir"; fi
if [[ $KERNEL_FAMILY == raspberrypi ]]; then bash scripts/install-raspi-boot.sh "$BOARD_ID" "$rootfs_dir"; fi
# Keep the shipped image lean. Run this after board-specific kernel/boot package
# installation, because those steps refresh APT indexes and download packages.
# The removed files are reproducible build-time state; apt update recreates them.
rm -rf "$rootfs_dir/var/lib/apt/lists/"* \
"$rootfs_dir/var/cache/apt/"* \
"$rootfs_dir/var/cache/debconf/"*-old \
"$rootfs_dir/var/cache/man/"* \
"$rootfs_dir/var/log/"*.log \
"$rootfs_dir/var/log/apt/"*
mkdir -p "$rootfs_dir/var/lib/apt/lists/partial" "$rootfs_dir/var/cache/apt/archives/partial"
# LeanPi Base is an appliance image, not an offline documentation host. Keep
# copyright/license metadata, but remove package changelogs, man pages, info
# pages and unused locale catalogs. Debian packages can restore them on demand.
find "$rootfs_dir/usr/share/doc" -type f \( -name '*.gz' -o -name changelog -o -name changelog.Debian -o -name changelog.Debian.gz \) -delete 2>/dev/null || true
rm -rf "$rootfs_dir/usr/share/man/"* "$rootfs_dir/usr/share/info/"* "$rootfs_dir/usr/share/locale/"*
mkdir -p "$rootfs_dir/usr/share/man" "$rootfs_dir/usr/share/info" "$rootfs_dir/usr/share/locale"
# Debian kernel packages install modules for many unrelated ARM boards. Keep
# module metadata intact for now; report the largest directories so size
# optimization can be evidence-driven rather than deleting hardware support.
echo "LeanPi rootfs size diagnostics:"
du -x -B1 -d1 "$rootfs_dir/usr" "$rootfs_dir/lib" "$rootfs_dir/var" 2>/dev/null | sort -nr | head -n 20 || true
for dir in "$rootfs_dir/usr/share" "$rootfs_dir/var/cache"; do
if [[ -d "$dir" ]]; then
du -x -B1 -d1 "$dir" 2>/dev/null | sort -nr | head -n 20 || true
fi
done
if [[ -d "$rootfs_dir/lib/modules" ]]; then
du -x -B1 -d2 "$rootfs_dir/lib/modules" 2>/dev/null | sort -nr | head -n 20 || true
fi
loopdev=; mnt=; cleanup(){ set +e; [[ -n $mnt ]] && mountpoint -q "$mnt" && umount "$mnt"; [[ -n $mnt && -d $mnt ]] && rmdir "$mnt"; [[ -n $loopdev ]] && losetup -d "$loopdev"; }; trap cleanup EXIT
loopdev=$(losetup --find --show --partscan "$image_path"); mkfs.ext4 -F -L leanpi-root "${loopdev}p1" >/dev/null; mnt=$(mktemp -d); mount "${loopdev}p1" "$mnt"; rsync -aHAX --numeric-ids "$rootfs_dir/" "$mnt/"; sync; umount "$mnt"; rmdir "$mnt"; mnt=; losetup -d "$loopdev"; loopdev=
echo 'LeanPi M1.2 image assembly: PASS'; echo "Image: $image_path"; du -sh "$rootfs_dir"; sha256sum "$image_path" | tee "${image_path}.sha256"