2
0

make-iso.sh 14 KB

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