2
0

make-iso.sh 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. #!/usr/bin/env bash
  2. # Builds mjdev-desktop-<version>.iso: a minimal, bootable Debian (latest stable)
  3. # live image carrying only wayland + the mjdev desktop — no X server, no desktop
  4. # environment, no recommends. It enables the non-free driver/firmware set, installs
  5. # every *.deb from deb-packages/ (plymouth + cursor + sound themes) and activates
  6. # them, and autostarts the mjdev wayland session on boot.
  7. #
  8. # Name + version come from the gradle version catalog (never hardcoded). The
  9. # desktop app deb and the compositor/session files must already be built — the
  10. # gradle `makeIso` task wires those dependencies and passes their paths in.
  11. # This script only assembles the image and must run as root (debootstrap +
  12. # chroot + mksquashfs).
  13. #
  14. # sudo ./make-iso.sh # uses the built deb + compositor + deb-packages/
  15. # sudo ./make-iso.sh --suite trixie # pin a specific debian suite
  16. set -euo pipefail
  17. HERE="$(cd "$(dirname "$0")" && pwd)"
  18. CATALOG="$HERE/gradle/libs.versions.toml"
  19. read_catalog() { sed -n "s/^$1 *= *\"\(.*\)\"/\1/p" "$CATALOG"; }
  20. APP_NAME="$(read_catalog app-name)"
  21. VERSION="$(read_catalog app-pkg-version)"
  22. [ -n "$APP_NAME" ] && [ -n "$VERSION" ] || { echo "cannot read app name/version from $CATALOG"; exit 1; }
  23. # wayland runtime stack for mjdevc — single source of truth in the version catalog (not hardcoded)
  24. RUNTIME_DEPS="$(read_catalog app-compositor-runtime-deps)"
  25. # apt-get must wait for the dpkg lock instead of failing ("Could not get lock
  26. # /var/lib/dpkg/lock-frontend") when another apt/dpkg runs — value from the catalog.
  27. APT_LOCK_TIMEOUT="$(read_catalog app-apt-lock-timeout)"
  28. # ---- config / args -------------------------------------------------------
  29. # trixie = Debian 13, the current stable ("latest public version"). A bare
  30. # "stable" can symlink to sid on non-debian hosts, so a concrete codename is used.
  31. SUITE="${MJDEV_ISO_SUITE:-trixie}"
  32. MIRROR="${MJDEV_ISO_MIRROR:-http://deb.debian.org/debian}"
  33. # building a debian rootfs from a non-debian host needs the debian archive key
  34. KEYRING="/usr/share/keyrings/debian-archive-keyring.gpg"
  35. # the gradle task overrides these; the defaults match the in-tree build outputs
  36. DEB="${MJDEV_ISO_DEB:-$(ls "$HERE"/packages/main-release/deb/*.deb 2>/dev/null | head -n1 || true)}"
  37. COMPOSITOR_BIN="${MJDEV_ISO_COMPOSITOR_BIN:-$HERE/compositor/build/session-install/mjdevc}"
  38. SESSION_DIR="${MJDEV_ISO_SESSION_DIR:-$HERE/compositor/build/session-install}"
  39. EXTRA_DEBS_DIR="${MJDEV_ISO_EXTRA_DEBS:-$HERE/deb-packages}"
  40. OUT="${MJDEV_ISO_OUT:-$HERE/releases/$APP_NAME-$VERSION.iso}"
  41. LIVE_USER="mjdev"
  42. WORK=""
  43. while [ $# -gt 0 ]; do
  44. case "$1" in
  45. --deb) DEB="$2"; shift 2;;
  46. --compositor-bin) COMPOSITOR_BIN="$2"; shift 2;;
  47. --session-dir) SESSION_DIR="$2"; shift 2;;
  48. --extra-debs) EXTRA_DEBS_DIR="$2"; shift 2;;
  49. --out) OUT="$2"; shift 2;;
  50. --suite) SUITE="$2"; shift 2;;
  51. --mirror) MIRROR="$2"; shift 2;;
  52. --work) WORK="$2"; shift 2;;
  53. -h|--help) sed -n '2,16p' "$0"; exit 0;;
  54. *) echo "unknown arg: $1"; exit 1;;
  55. esac
  56. done
  57. # tee everything to a build log so failures are diagnosable even when the script
  58. # is run through pkexec (which detaches the inherited stdio from the caller).
  59. LOG="${MJDEV_ISO_LOG:-/tmp/mjdev-iso-build.log}"
  60. exec > >(tee "$LOG") 2>&1
  61. echo ">> mjdev iso build log -> $LOG"
  62. [ "$(id -u)" -eq 0 ] || { echo "must run as root (debootstrap/chroot)"; exit 1; }
  63. [ -n "$DEB" ] && [ -f "$DEB" ] || { echo "desktop deb not found: '${DEB:-}'"; echo "build it first: ./gradlew :desktopApp:packageReleaseDeb"; exit 1; }
  64. [ -f "$COMPOSITOR_BIN" ] || { echo "compositor binary not found: $COMPOSITOR_BIN"; echo "build it first: ./gradlew :compositor:stageSession"; exit 1; }
  65. for t in debootstrap mksquashfs xorriso grub-mkrescue; do
  66. command -v "$t" >/dev/null 2>&1 || { echo "missing tool: $t (apt install debootstrap squashfs-tools xorriso grub-common grub-pc-bin grub-efi-amd64-bin)"; exit 1; }
  67. done
  68. DEB="$(readlink -f "$DEB")"
  69. COMPOSITOR_BIN="$(readlink -f "$COMPOSITOR_BIN")"
  70. SESSION_DIR="$(readlink -f "$SESSION_DIR")"
  71. mkdir -p "$(dirname "$OUT")"; OUT="$(readlink -f "$OUT")"
  72. [ -d "$EXTRA_DEBS_DIR" ] && EXTRA_DEBS_DIR="$(readlink -f "$EXTRA_DEBS_DIR")" || EXTRA_DEBS_DIR=""
  73. WORK="${WORK:-$(mktemp -d /tmp/mjdev-iso.XXXXXX)}"
  74. ROOT="$WORK/rootfs"; ISO="$WORK/iso"
  75. mkdir -p "$ROOT" "$ISO/live" "$ISO/boot/grub"
  76. cleanup() { umount -lf "$ROOT/dev/pts" "$ROOT/dev" "$ROOT/proc" "$ROOT/sys" 2>/dev/null || true; }
  77. trap cleanup EXIT
  78. # ---- 1. minimal base -----------------------------------------------------
  79. echo ">> debootstrap $SUITE (minbase) -> $ROOT"
  80. DEBOOTSTRAP_OPTS="--variant=minbase"
  81. [ -f "$KEYRING" ] && DEBOOTSTRAP_OPTS="$DEBOOTSTRAP_OPTS --keyring=$KEYRING"
  82. # shellcheck disable=SC2086
  83. debootstrap $DEBOOTSTRAP_OPTS "$SUITE" "$ROOT" "$MIRROR"
  84. mount --bind /dev "$ROOT/dev"
  85. mount -t devpts devpts "$ROOT/dev/pts" 2>/dev/null || true
  86. mount -t proc proc "$ROOT/proc"
  87. mount -t sysfs sys "$ROOT/sys"
  88. # the fresh chroot has no DNS resolver — copy the host's so apt-get inside the
  89. # chroot can reach the mirror (without this apt-get update fails -> exit 100).
  90. cp -L /etc/resolv.conf "$ROOT/etc/resolv.conf" 2>/dev/null || true
  91. cat > "$ROOT/etc/apt/apt.conf.d/99lean" <<EOF
  92. APT::Install-Recommends "false";
  93. APT::Install-Suggests "false";
  94. Acquire::Languages "none";
  95. DPkg::Lock::Timeout "$APT_LOCK_TIMEOUT";
  96. EOF
  97. # stage the desktop app deb, the compositor binary + session files, and every
  98. # extra deb (themes) into the chroot
  99. cp "$DEB" "$ROOT/tmp/mjdev-desktop.deb"
  100. install -Dm755 "$COMPOSITOR_BIN" "$ROOT/tmp/session/mjdevc"
  101. [ -f "$SESSION_DIR/mjdev-session" ] && install -Dm755 "$SESSION_DIR/mjdev-session" "$ROOT/tmp/session/mjdev-session"
  102. [ -f "$SESSION_DIR/mjdev.desktop" ] && install -Dm644 "$SESSION_DIR/mjdev.desktop" "$ROOT/tmp/session/mjdev.desktop"
  103. if [ -n "$EXTRA_DEBS_DIR" ] && ls "$EXTRA_DEBS_DIR"/*.deb >/dev/null 2>&1; then
  104. mkdir -p "$ROOT/tmp/extra-debs"
  105. cp "$EXTRA_DEBS_DIR"/*.deb "$ROOT/tmp/extra-debs/"
  106. fi
  107. # ---- 2-3. provision: kernel + wayland + our debs + theme activation + autostart
  108. cat > "$ROOT/tmp/provision.sh" <<PROVISION
  109. #!/bin/sh
  110. set -eu
  111. export DEBIAN_FRONTEND=noninteractive
  112. echo "$APP_NAME" > /etc/hostname
  113. # enable contrib + non-free + non-free-firmware so the full non-free driver and
  114. # firmware set is installable (minbase only enables main). deb822 or one-line,
  115. # whichever the base wrote.
  116. if [ -f /etc/apt/sources.list.d/debian.sources ]; then
  117. sed -i 's/^Components:.*/Components: main contrib non-free non-free-firmware/' \
  118. /etc/apt/sources.list.d/debian.sources
  119. else
  120. echo "deb $MIRROR $SUITE main contrib non-free non-free-firmware" > /etc/apt/sources.list
  121. fi
  122. apt-get update
  123. # kernel + live boot + bare wayland runtime + plymouth (boot splash). NO xorg,
  124. # NO desktop environment.
  125. apt-get install --no-install-recommends -y \
  126. linux-image-amd64 live-boot systemd-sysv \
  127. dbus dbus-user-session seatd \
  128. plymouth plymouth-label \
  129. libgl1-mesa-dri libglx-mesa0 libegl-mesa0 \
  130. fontconfig fonts-dejavu-core
  131. # the wayland compositor runtime stack mjdevc is linked against. libwlroots-0.18 pulls
  132. # its whole chain (libinput, libdrm, libgbm, libseat, libxkbcommon, libwayland-*,
  133. # libdisplay-info, libliftoff, libpixman, ...). XWayland: the AWT-based Compose shell
  134. # needs an X display, which mjdevc provides via its built-in XWayland (this is the
  135. # X-on-wayland shim, NOT a full xorg server). libegl1/libgles2 = the glvnd GL dispatch
  136. # the renderer uses. WITHOUT these mjdevc fails to even load (libwlroots-0.18.so missing)
  137. # -> black screen + tty1 autologin crash-loop.
  138. apt-get install --no-install-recommends -y $RUNTIME_DEPS
  139. # non-free GPU/wifi drivers + firmware so real hardware gets accelerated GL and
  140. # working radios (in a VM mesa still falls back to llvmpipe, which the session
  141. # allows). "|| true": some firmware metapackages are absent on some mirrors —
  142. # never fail the build for them.
  143. apt-get install --no-install-recommends -y \
  144. mesa-va-drivers mesa-vulkan-drivers || true
  145. apt-get install --no-install-recommends -y \
  146. firmware-linux firmware-linux-nonfree firmware-misc-nonfree \
  147. firmware-iwlwifi firmware-realtek firmware-atheros || true
  148. # our desktop deb is built by jpackage on an ubuntu host, so its auto-generated
  149. # Depends carry one ubuntu-only name — libjpeg-turbo8 (ubuntu's libjpeg.so.8),
  150. # which does not exist in debian (debian ships libjpeg62-turbo). the app is
  151. # self-contained (bundled jre + skiko, which carries its own image codecs), so the
  152. # dep is spurious. strip it from the control file before installing — that leaves a
  153. # fully-satisfiable package, so apt resolves the rest normally and never ends up in
  154. # a permanently "broken" state that would block apt autoremove during cleanup.
  155. dpkg-deb -R /tmp/mjdev-desktop.deb /tmp/deb-fix
  156. sed -i -E 's/, *libjpeg-turbo8//; s/libjpeg-turbo8, *//; s/^(Depends:) *libjpeg-turbo8 *\$/\1/' \
  157. /tmp/deb-fix/DEBIAN/control
  158. # the deb's postinst runs "xdg-desktop-menu install", which fails in a minbase chroot
  159. # (no desktop-environment menu infrastructure). the menu entry is cosmetic — the session
  160. # execs /opt/mjdev-desktop directly — so make that call non-fatal instead of fighting
  161. # xdg-desktop-menu's DE detection. we still drop the .desktop into /usr/share/applications.
  162. mkdir -p /usr/share/applications
  163. sed -i 's/^xdg-desktop-menu install .*/& || true/' /tmp/deb-fix/DEBIAN/postinst
  164. dpkg-deb -b /tmp/deb-fix /tmp/mjdev-desktop-fixed.deb
  165. apt-get install --no-install-recommends -y /tmp/mjdev-desktop-fixed.deb
  166. install -Dm644 /opt/mjdev-desktop/lib/mjdev-desktop-mjdev-desktop.desktop \
  167. /usr/share/applications/mjdev-desktop.desktop 2>/dev/null || true
  168. # every extra deb from deb-packages/ (plymouth/cursor/sound themes). their deps
  169. # (plymouth, plymouth-label) are already in the base; their postinst scripts register
  170. # the update-alternatives activated below.
  171. if ls /tmp/extra-debs/*.deb >/dev/null 2>&1; then
  172. apt-get install --no-install-recommends -y /tmp/extra-debs/*.deb
  173. fi
  174. # compositor binary + wayland session launcher + the wayland-sessions entry
  175. install -Dm755 /tmp/session/mjdevc /usr/bin/mjdevc
  176. [ -f /tmp/session/mjdev-session ] && install -Dm755 /tmp/session/mjdev-session /usr/bin/mjdev-session
  177. [ -f /tmp/session/mjdev.desktop ] && install -Dm644 /tmp/session/mjdev.desktop /usr/share/wayland-sessions/mjdev.desktop
  178. # ---- activate the themes -------------------------------------------------
  179. # plymouth: make mjdev the default boot theme and rebuild the initramfs so the
  180. # splash is embedded. -R rebuilds; fall back to the alternative + update-initramfs.
  181. if [ -f /usr/share/plymouth/themes/mjdev/mjdev.plymouth ]; then
  182. plymouth-set-default-theme -R mjdev 2>/dev/null || {
  183. update-alternatives --set default.plymouth \
  184. /usr/share/plymouth/themes/mjdev/mjdev.plymouth || true
  185. update-initramfs -u || true
  186. }
  187. fi
  188. # cursor: the postinst registered bloom under x-cursor-theme; select it and
  189. # export it for wayland clients (which read XCURSOR_THEME, not the X alternative).
  190. if [ -f /usr/share/icons/bloom/cursor.theme ]; then
  191. update-alternatives --set x-cursor-theme /usr/share/icons/bloom/cursor.theme || true
  192. echo 'XCURSOR_THEME=bloom' >> /etc/environment
  193. fi
  194. # sound: point the freedesktop sound theme at mjdev for libcanberra / our shell.
  195. if [ -d /usr/share/sounds/mjdev ]; then
  196. echo 'SOUND_THEME=mjdev' >> /etc/environment
  197. echo 'MJDEV_SOUND_THEME=mjdev' >> /etc/environment
  198. fi
  199. # passwordless live user, autologin tty1, straight into the wayland session
  200. useradd -m -s /bin/bash $LIVE_USER
  201. passwd -d $LIVE_USER
  202. # the compositor opens /dev/dri/card0 and /run/seatd.sock (group "video"); without
  203. # these groups the user gets no DRM seat and the session is a black screen. usermod,
  204. # not adduser — minbase ships usermod (passwd) but not the adduser wrapper.
  205. usermod -aG video,input,render $LIVE_USER
  206. getent group seat >/dev/null 2>&1 && usermod -aG seat $LIVE_USER || true
  207. systemctl enable seatd
  208. mkdir -p /etc/systemd/system/getty@tty1.service.d
  209. cat > /etc/systemd/system/getty@tty1.service.d/autologin.conf <<EOF
  210. [Service]
  211. ExecStart=
  212. ExecStart=-/sbin/agetty --autologin $LIVE_USER --noclear %I \\\$TERM
  213. EOF
  214. cat > /home/$LIVE_USER/.bash_profile <<EOF
  215. # start the mjdev desktop on boot (tty1 only)
  216. if [ -z "\\\$WAYLAND_DISPLAY" ] && [ "\\\$(tty)" = "/dev/tty1" ]; then
  217. exec mjdev-session
  218. fi
  219. EOF
  220. chown $LIVE_USER:$LIVE_USER /home/$LIVE_USER/.bash_profile
  221. PROVISION
  222. chmod +x "$ROOT/tmp/provision.sh"
  223. chroot "$ROOT" /tmp/provision.sh
  224. # ---- 4. clean_system: strip everything not needed -> smallest image ------
  225. clean_system() {
  226. echo ">> clean_system: stripping docs, locales, caches, autoremove"
  227. cat > "$ROOT/tmp/clean.sh" <<'CLEAN'
  228. #!/bin/sh
  229. set -eu
  230. export DEBIAN_FRONTEND=noninteractive
  231. apt-get -y autoremove --purge
  232. apt-get -y clean
  233. rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
  234. rm -rf /usr/share/doc/* /usr/share/man/* /usr/share/info/*
  235. find /usr/share/locale -mindepth 1 -maxdepth 1 ! -name 'en*' ! -name 'cs*' \
  236. -exec rm -rf {} + 2>/dev/null || true
  237. rm -f /etc/apt/apt.conf.d/99lean
  238. CLEAN
  239. chmod +x "$ROOT/tmp/clean.sh"
  240. chroot "$ROOT" /tmp/clean.sh
  241. rm -rf "$ROOT/tmp/provision.sh" "$ROOT/tmp/clean.sh" \
  242. "$ROOT/tmp/mjdev-desktop.deb" "$ROOT/tmp/session" "$ROOT/tmp/extra-debs"
  243. }
  244. clean_system
  245. # ---- 5. squash + assemble bootable iso -----------------------------------
  246. echo ">> exporting kernel + initrd"
  247. cp "$ROOT"/boot/vmlinuz-* "$ISO/live/vmlinuz"
  248. cp "$ROOT"/boot/initrd.img-* "$ISO/live/initrd.img"
  249. cleanup
  250. echo ">> mksquashfs (bulk of the time)"
  251. # xz + x86 BCJ filter = smallest squashfs for an amd64 rootfs
  252. mksquashfs "$ROOT" "$ISO/live/filesystem.squashfs" \
  253. -comp xz -Xbcj x86 -b 1M -noappend -e boot
  254. cat > "$ISO/boot/grub/grub.cfg" <<'EOF'
  255. set default=0
  256. set timeout=3
  257. menuentry "mjdev desktop (live)" {
  258. linux /live/vmlinuz boot=live quiet splash
  259. initrd /live/initrd.img
  260. }
  261. EOF
  262. echo ">> grub-mkrescue -> $OUT"
  263. grub-mkrescue -o "$OUT" "$ISO"
  264. # we run as root (pkexec/sudo); hand the iso back to the invoking user so it isn't
  265. # a root-owned file sitting in releases/. PKEXEC_UID (pkexec) / SUDO_UID (sudo).
  266. OWNER_UID="${PKEXEC_UID:-${SUDO_UID:-}}"
  267. [ -n "$OWNER_UID" ] && chown "$OWNER_UID":"$OWNER_UID" "$OUT" 2>/dev/null || true
  268. echo ">> done: $OUT ($(du -h "$OUT" | cut -f1))"
  269. # the iso is built — drop the (root-owned) scratch rootfs so it doesn't pile up
  270. # in /tmp. on failure WORK is kept (the trap only unmounts) for debugging.
  271. rm -rf "$WORK"