364c06e99cede89e57da4f9f39ba0be9b83ae98a
[shellsnippets/shellsnippets.git] / posix / mkrpi3b+img.sh
1 #!/bin/sh
2 #-
3 # Copyright © 2019
4 #       mirabilos <t.glaser@tarent.de>
5 #
6 # Provided that these terms and disclaimer and all copyright notices
7 # are retained or reproduced in an accompanying document, permission
8 # is granted to deal in this work without restriction, including un‐
9 # limited rights to use, publicly perform, distribute, sell, modify,
10 # merge, give away, or sublicence.
11 #
12 # This work is provided “AS IS” and WITHOUT WARRANTY of any kind, to
13 # the utmost extent permitted by applicable law, neither express nor
14 # implied; without malicious intent or gross negligence. In no event
15 # may a licensor, author or contributor be held liable for indirect,
16 # direct, other damage, loss, or other issues arising in any way out
17 # of dealing in the work, even if advised of the possibility of such
18 # damage or existence of a defect, except proven that it results out
19 # of said person’s immediate fault when using the work as intended.
20 #-
21 # Installs a Raspberry Pi 3B+ image from scratch (tested on a Debian
22 # bullseye/sid host system, others should work as well given suitab‐
23 # ly up-to-date tools), using qemu-user/binfmt_misc emulation to run
24 # the foreign architecture steps in a chroot.
25 #
26 # This currently does not set up swap. You can do that yourself with
27 # a swap file:
28 #  sudo fallocate -l 2GiB /PAGEFILE.SYS
29 #  sudo chown 0:0 /PAGEFILE.SYS
30 #  sudo chmod 600 /PAGEFILE.SYS
31 #  sudo mkswap /PAGEFILE.SYS
32 # and add a line “/PAGEFILE.SYS swap swap sw 0 0” to fstab(5).
33
34 #########
35 # SETUP #
36 #########
37
38 ht='    '
39 nl='
40 '
41 IFS=" $ht$nl"
42 POSIXLY_CORRECT=1
43 export POSIXLY_CORRECT
44 LANGUAGE=C
45 LC_ALL=C.UTF-8
46 export LC_ALL
47 unset LANGUAGE
48 safe_PATH=/bin:/sbin:/usr/bin:/usr/sbin
49 PATH=$PATH:$safe_PATH  # just to make sure
50 export safe_PATH PATH
51 d_arm64='64-bit ARMv8 (aarch64)'
52 d_armhf='32-bit ARMv7 with hardware Floating Point (fast)'
53 d_armel='32-bit ARMv5 with software FPU (slow), untested'
54
55 needprintf=
56 if test -n "$KSH_VERSION"; then
57         p() {
58                 typeset i
59                 for i in "$@"; do
60                         print -ru2 -- "$i"
61                 done
62         }
63 elif test x"$(printf '%s\n' 'a b' c 2>/dev/null)" = x"a b${nl}c"; then
64         p() {
65                 printf '%s\n' "$@" >&2
66         }
67 else
68         needprintf=y
69         p() {
70                 for p_arg in "$@"; do
71                         echo "$p_arg" >&2
72                 done
73         }
74 fi
75
76 ################################
77 # ERROR HANDLING AND UNWINDING #
78 ################################
79
80 T=
81 loopdev=
82 kpx=
83 mpt=
84 dieteardown() {
85         set -x
86         if test -n "$mpt"; then
87                 umount "$mpt/tmp"
88                 umount "$mpt/proc"
89                 umount "$mpt/dev/shm"
90                 umount "$mpt/dev/pts"
91                 umount "$mpt/boot/firmware"
92                 umount "$mpt"
93         fi
94         mpt=
95         if test -n "$kpx"; then
96                 kpartx -d -f -v -p p -t dos -s "$dvname"
97         fi
98         kpx=
99         if test -n "$loopdev"; then
100                 losetup -d "$loopdev"
101         fi
102         loopdev=
103 }
104 diecleanup() {
105         stty sane <&2 2>/dev/null
106         tput cnorm 2>/dev/null
107         tput sgr0 2>/dev/null
108         dieteardown
109         if test -n "$T"; then
110                 cd /
111                 rm -rf --one-file-system "$T"
112         fi
113         T=
114 }
115 die() {
116         pfx='E: '
117         for arg in "$@"; do
118                 p "$pfx$arg"
119                 pfx='N: '
120         done
121         diecleanup
122         exit 1
123 }
124 trap 'p "I: exiting, cleaning up…"; diecleanup; exit 0' EXIT
125 trap 'p "E: caught SIGHUP, cleaning up…"; diecleanup; exit 129' HUP
126 trap 'p "E: caught SIGINT, cleaning up…"; diecleanup; exit 130' INT
127 trap 'p "E: caught SIGQUIT, cleaning up…"; diecleanup; exit 131' QUIT
128 trap 'p "E: caught SIGPIPE, cleaning up…"; diecleanup; exit 141' PIPE
129 trap 'p "E: caught SIGTERM, cleaning up…"; diecleanup; exit 143' TERM
130
131 #########################
132 # PREREQUISITE CHECKING #
133 #########################
134
135 # ensure $TERM is set to something the chroot can use
136 case $TERM in
137 (Eterm|Eterm-color|ansi|cons25|cons25-debian|cygwin|dumb|hurd|linux|mach|mach-bold|mach-color|mach-gnu|mach-gnu-color|pcansi|rxvt|rxvt-basic|rxvt-m|rxvt-unicode|rxvt-unicode-256color|screen|screen-256color|screen-256color-bce|screen-bce|screen-s|screen-w|screen.xterm-256color|sun|vt100|vt102|vt220|vt52|wsvt25|wsvt25m|xterm|xterm-256color|xterm-color|xterm-debian|xterm-mono|xterm-r5|xterm-r6|xterm-vt220|xterm-xfree86)
138         # list from ncurses-base (6.1+20181013-2+deb10u1)
139         ;;
140 (screen.*|screen-*)
141         # aliases possibly from ncurses-term
142         TERM=screen ;;
143 (rxvt.*|rxvt-*)
144         # let’s hope…
145         TERM=rxvt ;;
146 (xterm.*|xterm-*)
147         # …this works…
148         TERM=xterm ;;
149 (linux.*)
150         # …probably
151         TERM=linux ;;
152 (*)
153         die "Your terminal type '$TERM' is not supported by ncurses-base." \
154             'Maybe run this script in GNU screen?' ;;
155 esac
156
157 # check that all utilities we use exist; give Debian paths for missing ones
158 rv=0
159 chkhosttool() {
160         chkhosttool_prog=$1; shift
161         chkhosttool_missing=0
162         for chkhosttool_fullpath in "$@"; do
163                 chkhosttool_basename=${chkhosttool_fullpath##*/}
164                 # POSIX way to check for utility (builtin or $PATH)
165                 if command -v "$chkhosttool_basename" >/dev/null 2>&1; then
166                         : # let’s hope it’s compatible with Debian’s
167                 else
168                         test $chkhosttool_missing = 1 || \
169                             p "E: please install $chkhosttool_prog to continue!"
170                         chkhosttool_missing=1
171                         p "N: missing: $chkhosttool_fullpath"
172                 fi
173         done
174         test $chkhosttool_missing = 0 || rv=1
175 }
176 chkhosttool bc /usr/bin/bc
177 chkhosttool binfmt-support /usr/sbin/update-binfmts
178 chkhosttool coreutils /bin/cat /bin/chmod /bin/chown /bin/cp /bin/dd \
179     /bin/ln /bin/mkdir /bin/mktemp /bin/rm /bin/stty /usr/bin/env \
180     /usr/bin/id /usr/bin/printf /usr/bin/truncate /usr/sbin/chroot
181 chkhosttool debootstrap /usr/sbin/debootstrap
182 chkhosttool dosfstools /sbin/mkfs.msdos
183 chkhosttool dpkg /usr/bin/dpkg-deb
184 chkhosttool e2fsprogs /sbin/mkfs.ext4
185 chkhosttool eatmydata /usr/bin/eatmydata
186 chkhosttool fdisk /sbin/fdisk
187 chkhosttool kpartx /sbin/kpartx
188 chkhosttool mount /bin/mount /bin/umount /sbin/losetup
189 chkhosttool ncurses-bin /usr/bin/tput
190 chkhosttool qemu-user-static /usr/bin/qemu-aarch64-static \
191     /usr/bin/qemu-arm-static
192 chkhosttool util-linux /bin/lsblk /sbin/fstrim /usr/bin/unshare
193 chkhosttool whiptail /usr/bin/whiptail
194 unset chkhosttool_prog
195 unset chkhosttool_missing
196 unset chkhosttool_fullpath
197 unset chkhosttool_basename
198 test x"$rv" = x"0" || exit "$rv"
199 if test -n "$needprintf"; then
200         p() {
201                 printf '%s\n' "$@" >&2
202         }
203         unset p_arg
204 fi
205 unset needprintf
206
207 # needs direct device I/O and chroot
208 case $(id -u) in
209 (0) ;;
210 (*) die 'Please run this as root.' ;;
211 esac
212
213 # create temporary directory as base of operations
214 T=$(mktemp -d /tmp/mkrpi3b+img.XXXXXXXXXX) || \
215     die 'cannot create temporary directory'
216 case $T in
217 (/*) ;;
218 (*) die "non-absolute temporary directory: $T" ;;
219 esac
220 chmod 700 "$T" || die 'chmod failed'
221 cd "$T" || die 'cannot cd into temporary directory'
222
223 #########################
224 # DIALOGUE PREPARATIONS #
225 #########################
226
227 # syntax: assign tgtvar fallback glob
228 assign() {
229         assign_tgt=$1; shift
230         assign_nil=$1; shift
231         eval "$assign_tgt=\$assign_nil"
232         test -n "$1" && test -e "$1" || return 0
233         eval "$assign_tgt=\$*"
234 }
235
236 # wrap around whiptail
237 w() {
238         whiptail --backtitle 'mkrpi3b+img.sh' --output-fd 4 4>res "$@"
239         rv=$?
240         res=$(cat res) || die cannot read whiptail result file
241         return $rv
242 }
243 # w plus advance the state machine
244 dw() {
245         if w "$@"; then
246                 s=$(($s+1))
247         elif test x"$s" = x"0"; then
248                 p '' 'I: aborted by user'
249                 diecleanup
250                 exit $rv
251         else
252                 s=$(($s-1))
253         fi
254         return $rv
255 }
256
257 ################################
258 # ENSURE MINIMUM TERMINAL SIZE #
259 ################################
260
261 s=0
262 while test x"$s" != x"9"; do
263         set -- $(stty size) || die 'stty failed'
264         test $# -eq 2 || die 'stty weird output' "$@"
265         case "$*" in
266         (*[!\ 0-9]*) die 'stty invalid output' "$@" ;;
267         esac
268         case $s in
269         (0)
270                 #### INITIAL TERMINAL SIZE CHECK
271                 s=4
272                 test $1 -ge 24 || s=1
273                 test $2 -ge 80 || s=1
274                 ;;
275         (1)
276                 #### TTY TOO SMALL REQUEST CHANGE
277                 p 'E: tty size too small' \
278                   "N: ${2}x$1 actual" "N: 80x24 minimum"
279                 sleep 5
280                 s=2
281                 ;;
282         (2)
283                 #### SEE WHETHER THAT HELPED
284                 s=4
285                 test $1 -ge 24 || s=3
286                 test $2 -ge 80 || s=3
287                 ;;
288         (3)
289                 #### STILL REQUEST CHANGE
290                 w --title 'Terminal size' --msgbox \
291                     "Your terminal is too small (only ${2}x$1 glyphs).
292
293 Please resize your terminal to at least 80x24 now, then press Enter to continue. Afterwards, DO *NOT* change the terminal size again! APT and debconf really do not like that, and your display will be garbled if you do…
294
295 Change size now, press Enter only afterwards to continue." 14 72
296                 s=5
297                 ;;
298         (4)
299                 #### INITIAL TTY SIZE OK
300                 w --title 'Terminal size' --msgbox \
301                     "Your terminal size is okay: ${2}x$1 glyphs, minimum 80x24 required.
302
303 Please DO *NOT* change the terminal size for the entire runtime of this script, starting now! APT and debconf really do not like that, and your display will be garbled if you do…
304
305 Press Enter to continue." 14 72
306                 s=5
307                 ;;
308         (5)
309                 #### SEE THAT WE END UP AT CORRECT SIZE
310                 s=9
311                 test $1 -ge 24 || s=6
312                 test $2 -ge 80 || s=6
313                 ;;
314         (6)
315                 #### NOT GOOD ENOUGH
316                 if w --title 'Terminal size' --yes-button OK --no-button Exit \
317                     --yesno "Your terminal still is too small (${2}x$1).
318
319 We requested you to change it to at least 80x24 (and keep the size constant afterwards). Please do so now, or press Escape or use the Exit button if you really can’t.
320
321 Remember to *NOT* change the size afterwards, since APT and debconf cache it and do not like sudden changes below them; if you do, your display will be garbled…
322
323 Change size now, press Enter only afterwards to continue." 17 72; then
324                         s=5
325                 else
326                         p '' 'I: aborted by user'
327                         exit 0
328                 fi
329                 ;;
330         esac
331 done
332
333 #################
334 # DIALOGUE LOOP #
335 #################
336
337 # default values
338 assign devices '' /dev/sd[a-z]
339 tgtdev=MANUAL
340 tgtimg=/dev/sdX
341 myfqdn=rpi3bplus.lan.tarent.invalid
342 userid=pi
343 setcma=                 # empty means yes (set CMA to higher value by default)
344 dropsd=--defaultno      # nonempty means no (do not drop systemd by default)
345 pkgadd=-                # - means out default values
346 tgarch=arm64
347 # state machine (menu question number)
348 s=0
349 while test x"$s" != x"10"; do
350         case $s in
351         (0)
352                 #### WHICH TARGET DEVICE? (CHOICE)
353                 set --
354                 for x in $devices; do
355                         set -- "$@" "$x" "$x"
356                 done
357                 if dw --title 'Select target device' \
358                     --notags --default-item "$tgtdev" \
359                     --menu 'Select which device to write to. IT WILL BE OVERWRITTEN!' \
360                     20 72 10 MANUAL 'Enter path manually (/dev/XXX or image file)' "$@"; then
361                         tgtdev=$res
362                         if test x"$tgtdev" = x"MANUAL"; then
363                                 tgtimg=/dev/sdX
364                         else
365                                 tgtimg=$tgtdev
366                                 s=$(($s+1))
367                         fi
368                 fi
369                 ;;
370         (1)
371                 #### WHICH TARGET DEVICE OR IMAGE? (FREETEXT)
372                 if dw --title 'Choose target device' \
373                     --inputbox 'Enter path to target raw device (e.g. /dev/sdX) or to pre-existing, already correctly-sized, image file to use:' \
374                     20 72 "$tgtimg"; then
375                         tgtimg=$res
376                 fi
377                 ;;
378         (2)
379                 #### VALIDATE IMAGE/DEVICE PATH/SIZE/ETC. / CREATE SPARSE FILE
380                 # step to go back to if things fail
381                 if test x"$tgtdev" = x"MANUAL"; then
382                         s=1
383                 else
384                         s=0
385                 fi
386                 # check image/device: path, existence, not a symlink (for stability)
387                 case $tgtimg in
388                 (/[!/]*) ;;
389                 (*)
390                         w --msgbox 'The chosen device/image path is not an absolute pathname!' 8 72
391                         continue ;;
392                 esac
393                 test -e "$tgtimg" || if w --title 'Nonexistent path chosen' \
394                     --ok-button 'Create' --cancel-button 'Go back' \
395                     --inputbox 'The chosen device/image path does not exist!
396
397 If you wish to create it, enter a size in the format accepted by GNU coreutils’ truncate(1) utility (that is, a number followed by M for MiB or G for GiB, in its most basic form) and select the “Create” button (or just press Enter). This will create a sparse image file (which will only take up the space actually used by data). We have pre-filled the input field with the minimum allowed image size.
398
399 The image will be created *immediately* and never deleted!
400
401 If you do not with to create it, press Escape to go back instead.' \
402                     20 72 1536M; then
403                         truncate -s "$res" "$tgtimg" || \
404                             die 'failed to create sparse file'
405                         test -e "$tgtimg" || die 'sparse file not created'
406                 else
407                         continue  # go back
408                 fi
409                 if test -h "$tgtimg"; then
410                         tgtimg=$(readlink -f "$tgtimg") || die 'error in readlink -f'
411                         s=2 # redo from start
412                         continue
413                 fi
414                 # whether block device, regular file or grounds for refusal
415                 if test -b "$tgtimg"; then
416                         dvname=$tgtimg
417                 elif test -f "$tgtimg"; then
418                         # losetup -f does not echo chosen devicem which we need
419                         dvname=$(losetup -f) || die 'losetup failed in get'
420                         case $dvname in
421                         (/dev/loop*) ;;
422                         (*) die 'losetup shows weird result' "$dvname" ;;
423                         esac
424                         loopdev=$dvname
425                         losetup "$loopdev" "$tgtimg" || die 'losetup failed in set'
426                 else
427                         w --msgbox 'The chosen device/image path is neither a block special device nor a regular file!' 8 72
428                         continue
429                 fi
430                 # we now have a block (or loopback device) we can check
431                 sz=$(lsblk --nodeps --noheadings --output SIZE --bytes \
432                     --raw "$dvname") || die 'lsblk failed'
433                 case $sz in
434                 (*[!0-9]*) die 'lsblk shows weird result' "$sz" ;;
435                 ([0-9]*) ;;
436                 (*) die 'lsblk returned empty result' ;;
437                 esac
438                 # use bc for arithmetic: numbers too large for shell
439                 case $(echo "a=0; if($sz<(1536*1048576)) a=1; a" | bc) in
440                 (0) ;;
441                 (1)
442                         w --msgbox 'The chosen device/image path is smaller than 1536 MiB!' 8 72
443                         dieteardown
444                         continue ;;
445                 (*) die 'bc returned weird result' ;;
446                 esac
447                 # and it’s big enough for the Debian installation; accept
448                 s=2
449                 sz=$(echo "scale=2; $sz/1048576" | bc) || die 'bc cannot divide'
450                 dw --title 'Accept target device' --defaultno \
451                     --yesno "Your chosen target device: $tgtimg
452
453 Block device $dvname of size $sz MiB
454
455 Do you REALLY want to use this as target device
456 and OVERWRITE ALL DATA with no chance of recovery?" 14 72
457                 ;;
458         (3)
459                 #### HOSTNAME FOR THE SYSTEM
460                 if dw --title 'Enter target hostname' \
461                     --inputbox 'Enter fully-qualified hostname the target device should have:' \
462                     20 72 "$myfqdn"; then
463                         # one trailing full stop is allowed (like DNS)
464                         myfqdn=${res%.}
465                         # check length [1; 255]
466                         if test "${#myfqdn}" -lt 1; then
467                                 w --msgbox 'The given hostname is empty!' 8 72
468                                 s=3; continue  # retry
469                         fi
470                         if test "${#myfqdn}" -gt 255; then
471                                 w --msgbox 'The given hostname is too long!' 8 72
472                                 s=3; continue  # retry
473                         fi
474                         # check characters used
475                         case $myfqdn in
476                         (.*|*.)
477                                 w --msgbox 'The given hostname begins or ends with a full stop!' 8 72
478                                 s=3; continue ;;
479                         (*[!.0-9A-Za-z-]*)
480                                 w --msgbox 'The given hostname contains invalid characters!' 8 72
481                                 s=3; continue ;;
482                         esac
483                         # similar for the component labels
484                         IFS=.; set -o noglob
485                         set -- $myfqdn
486                         IFS=" $ht$nl"; set +o noglob
487                         for x in "$@"; do
488                                 if test "${#x}" -lt 1 || test "${#x}" -gt 63; then
489                                         w --msgbox 'The given hostname contains parts that are empty or too long!' 8 72
490                                         s=3; break
491                                 fi
492                                 # invalid label composition
493                                 case $x in
494                                 (-*)
495                                         w --msgbox 'The given hostname contains parts that begin with a hyphen-minus!' 8 72
496                                         s=3; break ;;
497                                 (*-)
498                                         w --msgbox 'The given hostname contains parts that end with a hyphen-minus!' 8 72
499                                         s=3; break ;;
500                                 (*[!0-9A-Za-z-]*)
501                                         w --msgbox 'The given hostname contains invalid characters!' 8 72
502                                         s=3; break ;;
503                                 esac
504                         done
505                         case $myfqdn in
506                         (*.local)
507                                 w --msgbox 'The given hostname uses the TLD reserved for mDNS!' 8 72
508                                 s=3 ;;
509                         esac
510                         # s is now 3=redo (msgbox shown) or 4=go on
511                 fi
512                 ;;
513         (4)
514                 #### USERNAME TO CREATE FOR INITIAL SSH AND SUDO
515                 if dw --title 'Enter initial username' \
516                     --inputbox 'Enter UNIX username of the initially created user (which has full sudo access):' \
517                     20 72 "$userid"; then
518                         userid=$res
519                         # Unix limitations
520                         if test -z "$userid"; then
521                                 w --msgbox 'The given username is empty!' 8 72
522                                 s=4; continue  # retry
523                         fi
524                         if test "${#userid}" -gt 32; then
525                                 w --msgbox 'The given username is too long! (32 bytes max.)' 8 72
526                                 s=4; continue  # retry
527                         fi
528                         # default /etc/adduser.conf NAME_REGEX
529                         case $userid in
530                         (*[!a-z0-9_-]*)
531                                 w --msgbox 'The given username contains invalid characters!' 8 72
532                                 s=4; continue ;;
533                         ([!a-z]*)
534                                 w --msgbox 'The given username does not start with a letter!' 8 72
535                                 s=4; continue ;;
536                         esac
537                 fi
538                 ;;
539         (5)
540                 #### ADJUST DEFAULT CMA SIZE?
541                 if w --title 'Default CMA size' \
542                     $setcma --yesno "Raise default CMA from 64 to 128 MiB?
543
544 This is especially useful when you’ll be using graphics." 10 72; then
545                         setcma=
546                         s=6
547                 elif test x"$rv" = x"1"; then
548                         setcma=--defaultno
549                         s=6
550                 else
551                         s=4
552                 fi
553                 ;;
554         (6)
555                 #### SELECT INIT SYSTEM
556                 if w --title 'Choose the init system' \
557                     $dropsd --yesno "Change init system from systemd to sysvinit?
558
559 The default init system in Debian 10 “buster” is systemd with usrmerge.
560 This option allows you to change to traditional SysV init with classic
561 filesystem layout.
562
563 Most users will say “No” here." 10 72; then
564                         dropsd=
565                         s=7
566                 elif test x"$rv" = x"1"; then
567                         dropsd=--defaultno
568                         s=7
569                 else
570                         s=5
571                 fi
572                 ;;
573         (7)
574                 #### ARCHITECTURE
575                 case $tgarch in
576                 (arm64) set -- on off off ;;
577                 (armhf) set -- off on off ;;
578                 (armel) set -- off off on ;;
579                 (*) die "huh? tgarch<$tgarch>" ;;
580                 esac
581                 if dw --title 'Choose target architecture' \
582                     --radiolist 'Please select which Debian architecture to install on the target. The default is usually fine, as you can run 32-bit binaries (both armel and armhf) under a 64-bit kernel normally, with Multi-Arch.
583
584 Use the cursor keys ↑ and ↓ followed by Space to select an item; press Enter to accept and continue, or Esc to go back.' \
585                    15 72 3 \
586                    arm64 "$d_arm64 " "$1" \
587                    armhf "$d_armhf " "$2" \
588                    armel "$d_armel " "$3"; then
589                         tgarch=$res
590                 fi
591                 ;;
592         (8)
593                 #### EXTRA PACKAGES TO INSTALL
594                 if test x"$pkgadd" = x"-"; then
595                         # openssh-server will generate the server keys using
596                         # random bytes from the host, in the chroot (good!)
597                         pkgadd='anacron bind9-host bridge-utils postfix bsd-mailx curl etckeeper ethtool ntp openssh-server patch pv rdate reportbug unscd wget _WLAN_'
598                         blurb=' We have provided you with a selection of default useful system utilities and services, which you can change if you wish, of course.'
599                 else
600                         blurb=
601                 fi
602                 if dw --title 'Extra packages to install' \
603                     --inputbox "Enter extra packages to install, separated by space.$blurb Some other extra packages, like less and sudo, are always installed.
604
605 You can use the macro _WLAN_ to select packages needed to support WiFi. To install packages from Backports, append “/buster-backports” to the package name, e.g. “musescore3/buster-backports”. Removing packages by appending “-” to their name is also possible, as this list is passed as-is to apt-get install.
606
607 Press ^U (Ctrl-U) to delete the entire line.
608 Enter just - to restore the default and start editing anew." \
609                     20 72 "$pkgadd"; then
610                         pkgadd=$res
611                         test x"$pkgadd" = x"-" && s=8
612                 fi
613                 ;;
614         (9)
615                 #### SUMMARY BEFORE DOING ANYTHING (except sparse file creation)
616                 if test -n "$setcma"; then cma=64; else cma=128; fi
617                 if test -n "$dropsd"; then
618                         init='systemd with usrmerge'
619                 else
620                         init='sysvinit with standard filesystem'
621                 fi
622                 case $tgarch in
623                 (arm64|armhf|armel) eval "arch=\"\$tgarch: \$d_$tgarch\"" ;;
624                 (*) die "huh? tgarch<$tgarch>" ;;
625                 esac
626                 dw --title 'Proceed with installation?' --defaultno \
627                     --yesno "Do you wish to proceed and DELETE ALL DATA from the target device? (Choosing “No” or pressing Escape allows you to go back to each individual step for changing the information.) Summary of settings:
628
629 Target  : $tgtimg
630 Hostname: $myfqdn
631 Username: $userid
632 CMA size: $cma MiB
633 init/FHS: $init
634 Machine : $arch
635
636 Packages: $pkgadd" 20 72
637                 ;;
638         esac
639 done
640
641 ##########################
642 # PREPARE DISC STRUCTURE #
643 ##########################
644
645 p 'I: ok, proceeding; this may take some time…' \
646   'N: be prepared to interactively answer more questions though'
647 sleep 3
648 # store some random seed for later, 1ˢᵗ half (taken from host CSPRNG)
649 dd if=/dev/urandom bs=256 count=1 of=rnd 2>/dev/null || die 'dd rnd1 failed'
650 # create MBR with empty BIOS partition table
651 s='This SD card boots on a Raspberry Pi 3B+ only!'
652 # x86 machine code outputting message then stopping
653 printf '\xE8\x'$(echo "obase=16; ${#s}+5" | bc)'\0\r\n' >data
654 printf '%s' 'This SD card boots on a Raspberry Pi 3B+ only!' | tee txt >>data
655 printf '\r\n\0\x5E\16\x1F\xAC\10\xC0\x74\xFE\xB4\16\xBB\7\0\xCD\20\xEB\xF2' \
656     >>data
657 dd if=/dev/zero bs=16 count=4 of=pt 2>/dev/null || die 'dd mbr1 failed'
658 printf '\x55\xAA' >>pt
659 # cobble together wiping partition first MiB “en passant”
660 dd if=/dev/urandom bs=256 count=4096 of=mbr 2>/dev/null || die 'dd mbr2 failed'
661 dd if=mbr bs=1048576 of="$dvname" seek=1 2>/dev/null || die 'dd clr1 failed'
662 dd if=mbr bs=1048576 of="$dvname" seek=128 2>/dev/null || die 'dd clr2 failed'
663 dd if=data of=mbr conv=notrunc 2>/dev/null || die 'dd mbr3 failed'
664 dd if=pt of=mbr bs=1 seek=446 conv=notrunc 2>/dev/null || die 'dd mbr4 failed'
665 # write to disc, wiping pre-partition space as well
666 dd if=mbr bs=1048576 of="$dvname" 2>/dev/null || die 'dd mbr5 failed'
667 rm mbr
668 # layout partition table (per board-specific requirements)
669 (fdisk -c=nondos -t MBR -w always -W always "$tgtimg" <<-'EOF'
670         n
671         p
672         1
673         2048
674         262143
675         n
676         p
677         2
678         262144
679
680         t
681         1
682         c
683         w
684         EOF
685 ) || die 'fdisk failed'
686 # map partitions so we can access them under a fixed name
687 kpx=/dev/mapper/${dvname##*/}
688 kpartx -a -f -v -p p -t dos -s "$dvname" || die 'kpartx failed'
689 # create filesystems
690 eatmydata mkfs.msdos -f 1 -F 32 -m txt -n RPi3BpFirmw -v "${kpx}p1" || \
691     die 'mkfs.msdos failed'
692 eatmydata mkfs.ext4 -e remount-ro -E discard -L RasPi3B+root \
693     -U random "${kpx}p2" || die 'mkfs.ext4 failed'
694 # mount filesystems
695 mpt=$T/mnt
696 mkdir "$mpt" || die 'mkdir mpt failed'
697 mount -t ext4 -o noatime,discard "${kpx}p2" "$mpt" || die 'mount (ext4) failed'
698 mkdir "$mpt/boot" || die 'mkdir mpt/boot failed'
699 mkdir "$mpt/boot/firmware" || die 'mkdir mpt/boot/firmware failed'
700 mount -t vfat -o noatime,discard "${kpx}p1" "$mpt/boot/firmware" || \
701     die 'mount (vfat) failed'
702
703 #################################################
704 # INSTALL DEBIAN, FIRST STAGE (CROSS-BOOTSTRAP) #
705 #################################################
706
707 p 'I: created filesystems, now debootstrapping…'
708 if test -n "$dropsd"; then
709         init=
710 else
711         init=--no-merged-usr
712 fi
713 case $tgarch in
714 (arm64) kernel=linux-image-arm64 qemu=qemu-aarch64-static ;;
715 (armhf) kernel=linux-image-armmp qemu=qemu-arm-static ;;
716 (armel) kernel=linux-image-rpi qemu=qemu-arm-static ;;
717 (*) die "huh? tgarch<$tgarch>" ;;
718 esac
719 # retrieve path to the command (its existence was tested earlier)
720 qemu_user_static=$(command -v $qemu) || die 'huh?'
721 case $qemu_user_static in
722 (/*) ;;
723 (*) die "$qemu cannot be found" ;;
724 esac
725 test -x "$qemu_user_static" || \
726     die "$qemu $qemu_user_static is not executable"
727
728 # added programs: eatmydata to speed up APT/dpkg; makedev needs to be
729 # run very early as we can’t use udev or the host’s /dev filesystem,
730 # mksh because the post-install script is written in it for simplicity
731 eatmydata debootstrap --arch=$tgarch --include=eatmydata,makedev,mksh $init \
732     --force-check-gpg --verbose --foreign buster "$mpt" \
733     http://deb.debian.org/debian sid || die 'debootstrap (first stage) failed'
734     # script specified here as it’s normally what buster symlinks to,
735     # to achieve compatibility with more host distros
736 # we need this early; Debian #700633
737 (
738         set -e
739         cd "$mpt"
740         for archive in var/cache/apt/archives/*eatmydata*.deb; do
741                 dpkg-deb --fsys-tarfile "$archive" >a
742                 tar -xkf a
743         done
744         rm -f a
745 ) || die 'failure extracting eatmydata early'
746 # the user can delete this later, from the booted system
747 cp "$qemu_user_static" "$mpt/usr/bin/$qemu" || die 'cp failed'
748
749 ##################################################
750 # INSTALL DEBIAN, SECOND STAGE (UNDER EMULATION) #
751 ##################################################
752
753 p 'I: second stage bootstrap (under emulation), slooow…'
754 mount -t tmpfs swap "$mpt/dev/shm" || die 'mount /dev/shm failed'
755 mount -t proc  proc "$mpt/proc" || die 'mount /proc failed'
756 mount -t tmpfs swap "$mpt/tmp" || die 'mount /tmp failed'
757 chroot "$mpt" /usr/bin/env -i LC_ALL=C.UTF-8 HOME=/root PATH="$safe_PATH" \
758     TERM="$TERM" /usr/bin/eatmydata /debootstrap/debootstrap --second-stage || \
759     die 'debootstrap (second stage) failed'
760 # debootstrap umounts some; just umount then remount everything
761 umount "$mpt/tmp" 2>/dev/null
762 umount "$mpt/proc" 2>/dev/null
763 umount "$mpt/dev/shm" 2>/dev/null
764
765 ####################################################################
766 # CREATE POST-BOOTSTRAP ENVIRONMENT AND ADJUST CONFIGURATION FILES #
767 ####################################################################
768
769 p 'I: pre-configuring…'
770 mount -t tmpfs swap "$mpt/dev/shm" || die 'remount /dev/shm failed'
771 mount -t proc  proc "$mpt/proc" || die 'remount /proc failed'
772 mount -t tmpfs swap "$mpt/tmp" || die 'remount /tmp failed'
773 # extra as needed below
774 mount --bind /dev/pts "$mpt/dev/pts" || die 'bind-mount /dev/pts failed'
775
776 # standard configuration files (generic)
777 if test -n "$dropsd"; then
778         # path apparently varies with init system
779         rnd=/var/lib/systemd/random-seed
780 else
781         # traditional path (content is identical though)
782         rnd=/var/lib/urandom/random-seed
783 fi
784 (
785         set -ex
786         # as set by d-i
787         printf '%s\n' '0.0 0 0.0' 0 UTC >"$mpt/etc/adjtime"
788         cat >"$mpt/etc/apt/sources.list" <<-'EOF'
789 deb http://deb.debian.org/debian buster main non-free contrib
790 deb http://deb.debian.org/debian-security buster/updates main non-free contrib
791 deb http://deb.debian.org/debian buster-updates main non-free contrib
792 deb http://deb.debian.org/debian buster-backports main non-free contrib
793         EOF
794         # from console-setup (1.193) config/keyboard (d-i)
795         cat >"$mpt/etc/default/keyboard" <<-'EOF'
796                 # KEYBOARD CONFIGURATION FILE
797
798                 # Consult the keyboard(5) manual page.
799
800                 XKBMODEL=pc105
801                 XKBLAYOUT=us
802                 XKBVARIANT=
803                 XKBOPTIONS=
804
805                 BACKSPACE=guess
806         EOF
807         # avoids early errors, configured properly later
808         : >"$mpt/etc/default/locale"
809         # target-appropriate
810         cat >"$mpt/etc/fstab" <<-'EOF'
811 LABEL=RasPi3B+root  /               ext4   defaults,relatime,discard       0  2
812 LABEL=RPi3BpFirmw   /boot/firmware  vfat   defaults,noatime,discard        0  1
813 swap                /tmp            tmpfs  defaults,relatime,nosuid,nodev  0  0
814         EOF
815         # hostname and hosts (generic)
816         case $myfqdn in
817         (*.*)   myhost="$myfqdn ${myfqdn%%.*}" ;;
818         (*)     myhost=$myfqdn ;;
819         esac
820         printf '%s\n' "$myfqdn" >"$mpt/etc/hostname"
821         cat >"$mpt/etc/hosts" <<-EOF
822                 127.0.0.1       $myhost localhost localhost.localdomain
823
824                 ::1     ip6-localhost ip6-loopback localhost6 localhost6.localdomain6
825                 fe00::0 ip6-localnet
826                 ff00::0 ip6-mcastprefix
827                 ff02::1 ip6-allnodes
828                 ff02::2 ip6-allrouters
829                 ff02::3 ip6-allhosts
830         EOF
831         # like d-i
832         rm -f "$mpt/etc/mtab"
833         ln -sfT /proc/self/mounts "$mpt/etc/mtab"
834         # so the user can ssh in straight after booting
835         cat >>"$mpt/etc/network/interfaces" <<-'EOF'
836
837                 # The loopback network interface
838                 auto lo
839                 iface lo inet loopback
840
841                 # First Ethernet interface
842                 auto eth0
843                 iface eth0 inet dhcp
844         EOF
845         # for bootstrapping in chroot
846         cat /etc/resolv.conf >"$mpt/etc/resolv.conf"
847         # base directory, init system-dependent but identical
848         mkdir -p "$mpt${rnd%/*}"
849         test -d "$mpt${rnd%/*}"/.
850         chown 0:0 "$mpt${rnd%/*}"
851         chmod 755 "$mpt${rnd%/*}"
852 ) || die 'pre-configuring failed'
853
854 ###################################
855 # CREATE POST-INSTALLATION SCRIPT #
856 ###################################
857
858 (
859         set -e
860         # beginning
861         cat <<-'EOF'
862                 #!/bin/mksh
863                 set -e
864                 set -o pipefail
865                 # reset environment so we can work
866                 unset LANGUAGE
867                 export DEBIAN_FRONTEND=teletype HOME=/root LC_ALL=C.UTF-8 \
868                     PATH=/usr/sbin:/usr/bin:/sbin:/bin POSIXLY_CORRECT=1
869                 export SUDO_USER=root USER=root # for etckeeper
870                 # necessary to avoid leaking the host’s /dev
871                 print -ru2 -- 'I: the MAKEDEV step is extremely slow…'
872                 set -x
873                 (cd /dev && exec MAKEDEV std sd console ttyS0)
874                 # because this is picked up by packages, e.g. postfix
875                 hostname "$(</etc/hostname)"
876                 # sanitise APT state
877                 apt-get clean
878                 apt-get update
879                 # for debconf (required)
880                 apt-get --purge -y install --no-install-recommends \
881                     libterm-readline-gnu-perl
882                 export DEBIAN_FRONTEND=readline
883                 # just in case there were security uploads
884                 apt-get --purge -y dist-upgrade
885         EOF
886         # switch to sysvinit?
887         test -n "$dropsd" || cat <<-'EOF'
888                 apt-get --purge -y install --no-install-recommends \
889                     sysvinit-core systemd-
890                 printf '%s\n' \
891                     'Package: systemd' 'Pin: version *' 'Pin-Priority: -1' '' \
892                     >/etc/apt/preferences.d/systemd
893                 # make it suck slightly less, mostly already in sid
894                 (: >/etc/init.d/.legacy-bootordering)
895                 grep FANCYTTY /etc/lsb-base-logging.sh >/dev/null 2>&1 || \
896                     echo FANCYTTY=0 >>/etc/lsb-base-logging.sh
897         EOF
898         # install base packages
899         echo "kernel=$kernel qemu=$qemu"
900         cat <<-'EOF'
901                 rm -f /var/cache/apt/archives/*.deb  # save temp space
902                 # kernel, initrd and base firmware
903                 apt-get --purge -y install --no-install-recommends \
904                     busybox firmware-brcm80211 firmware-linux-free \
905                     $kernel
906                 rm -f /var/cache/apt/archives/*.deb  # save temp space
907                 # some tools and bootloader firmware
908                 apt-get --purge -y install --no-install-recommends \
909                     adduser ed linuxlogo raspi3-firmware sudo whiptail
910                 rm -f /var/cache/apt/archives/*.deb  # save temp space
911                 export DEBIAN_FRONTEND=dialog
912                 # basic configuration
913                 print -r -- '(. /etc/os-release 2>/dev/null; linux_logo' \
914                     '-uy ${PRETTY_NAME+-t "OS version: $PRETTY_NAME"} || :)' \
915                     >/etc/profile.d/linux_logo.sh
916                 # user configuration
917                 (whiptail --backtitle 'mkrpi3b+img.sh' --msgbox \
918                     'We will now reconfigure some packages, so you can set up some basic things about your system: timezone (default UTC), keyboard layout, console font, and the system locale (and possibly whether additional locales are to be installed).
919
920 Press Enter to continue.' 12 72 || :)
921                 dpkg-reconfigure -plow tzdata
922                 rm -f /etc/default/locale  # force generation
923                 DEBIAN_PRIORITY=low \
924                     apt-get --purge -y install --no-install-recommends \
925                     console-{common,data,setup} locales
926                 # whether the user just hit Enter
927                 case $(</etc/default/locale) in
928                 (''|'#  File generated by update-locale')
929                         # empty, add sensible default (same as update-locale)
930                         print -r -- 'LANG=C.UTF-8' >>/etc/default/locale
931                         ;;
932                 esac
933         EOF
934         # adjust CMA size?
935         test -n "$setcma" || cat <<-'EOF'
936                 ed -s /etc/default/raspi3-firmware <<-'EODB'
937                         ,g/^#CMA=64M/s//CMA=128M/
938                         w
939                         q
940                 EODB
941                 # enact the changes
942                 /etc/initramfs/post-update.d/z50-raspi3-firmware
943         EOF
944         # remaining packages and configuration
945         cat <<-'EOF'
946                 : remaining user configuration may error out intermittently
947                 set +e
948                 # make man-db faster at cost of no apropos(1) lookup database
949                 debconf-set-selections <<-'EODB'
950                         man-db man-db/build-database boolean false
951                         man-db man-db/auto-update boolean false
952                 EODB
953                 : install basic packages  # change at your own risk but ok
954                 apt-get --purge -y install --no-install-recommends \
955                     bc ca-certificates ifupdown iproute2 jupp joe-jupp less \
956                     lsb-release lynx man-db mc mlocate molly-guard net-tools \
957                     netcat-openbsd openssh-client popularity-contest procps \
958                     rsync screen sharutils
959                 rm -f /var/cache/apt/archives/*.deb  # save temp space
960         EOF
961         set -o noglob
962         set -- $pkgadd
963         set +o noglob
964         pkgs= s=
965         for pkg in "$@"; do
966                 # macro substitution of tools often found together
967                 case $pkg in
968                 (_WLAN_) pkg='crda wireless-tools wpasupplicant' ;;
969                 esac
970                 # collect list of packages to install
971                 pkgs="$pkgs$s$pkg" s=' '
972         done
973         # list of groups from user-setup (1.81), i.e. d-i,
974         # debian/user-setup.templates: passwd/user-default-groups
975         set -- audio bluetooth cdrom debian-tor dip floppy lpadmin \
976             netdev plugdev scanner video
977         # we add adm and sudo (at least sudo done by d-i as well)
978         groups="$* adm sudo"
979         cat <<-EOF
980                 : install extra packages
981                 apt-get --purge install --no-install-recommends $pkgs
982                 rm -f /var/cache/apt/archives/*.deb  # save temp space
983                 : create initial user account, asking for password
984                 adduser '$userid'
985                 : ignore errors for nonexisting groups, please
986                 for group in $groups; do
987                         adduser '$userid' \$group
988                 done
989                 : end of pre-scripted post-bootstrap steps
990                 set +x
991                 # prepare for manual steps as desired
992                 userid='$userid'
993         EOF
994
995         ##############################################################
996         # PERMIT MANUAL STEPS BY SWITCHING (UNDER EMULATION) TO USER #
997         ##############################################################
998
999         cat <<-'EOF'
1000                 # instruct the user what they can do now
1001                 whiptail --backtitle 'mkrpi3b+img.sh' \
1002                     --msgbox "We will now (chrooted into the target system, under emulation, so it will be really slooooow…) run a login shell as the user account we just created ($userid), so you can do any manual post-installation steps desired.
1003
1004 Please use “sudo -S command” to run things as root, if necessary.
1005
1006 Press Enter to continue; exit the emulation with the “exit” command." 14 72
1007                 # clean environment for interactive use
1008                 unset DEBIAN_FRONTEND POSIXLY_CORRECT
1009                 export HOME=/  # later overridden by su
1010                 # create an initial entry in syslog
1011                 >>/var/log/syslog print -r -- "$(date +"%b %d %T")" \
1012                     "${HOSTNAME%%.*} mkrpi3b+img.sh[$$]:" \
1013                     soliciting manual post-installation steps
1014                 chown 0:adm /var/log/syslog
1015                 chmod 640 /var/log/syslog
1016                 # avoids warnings with sudo, cf. Debian #922349
1017                 find /usr/lib -name libeatmydata.so\* -a -type f -print0 | \
1018                     xargs -0r chmod u+s --
1019                 (unset SUDO_USER USER; exec su - "$userid")
1020                 # revert the above change again
1021                 find /usr/lib -name libeatmydata.so\* -a -type f -print0 | \
1022                     xargs -0r chmod u-s --
1023                 # might not do anything, but allow the user refusal
1024                 print -ru2 -- 'I: running apt-get autoremove,' \
1025                     'acknowledge as desired'
1026                 apt-get --purge autoremove
1027                 # remove installation debris
1028                 print -ru2 -- 'I: finally, cleaning up'
1029                 apt-get clean
1030                 pwck -s
1031                 grpck -s
1032                 rm -f /etc/{passwd,group,{,g}shadow,sub{u,g}id}-
1033                 # record initial /etc state
1034                 if whence -p etckeeper >/dev/null; then
1035                         etckeeper commit 'Finish installation'
1036                         etckeeper vcs gc
1037                 fi
1038                 rm -f /var/log/bootstrap.log
1039                 # from /lib/init/bootclean.sh
1040                 cd /run
1041                 find . ! -xtype d ! -name utmp ! -name innd.pid -delete
1042                 # fine𝄐
1043                 >>/var/log/syslog print -r -- "$(date +"%b %d %T")" \
1044                     "${HOSTNAME%%.*} mkrpi3b+img.sh[$$]:" \
1045                     finishing up installation\; once booted natively on the \
1046                     device, you can nuke /usr/bin/$qemu manually later
1047         EOF
1048 ) >"$mpt/root/munge-it.sh" || die 'post-installation script creation failure'
1049
1050 # now place initial random seed in the target location
1051 mv rnd "$mpt$rnd" || die 'mv rnd failed'
1052 chown 0:0 "$mpt$rnd" || die 'chown rnd failed'
1053 chmod 600 "$mpt$rnd" || die 'chmod rnd failed'
1054 # second half (collected from host’s CSPRNG now)
1055 dd if=/dev/urandom bs=256 count=1 >>"$mpt$rnd" || die 'dd rnd2 failed'
1056
1057 ########################################################
1058 # POST-BOOTSTRAP SCRIPT RUN IN CHROOT, UNDER EMULATION #
1059 ########################################################
1060
1061 # run the script concatenated together above in the chroot
1062 unshare --uts chroot "$mpt" /usr/bin/env -i TERM="$TERM" /usr/bin/eatmydata \
1063     /bin/mksh /root/munge-it.sh || die 'post-bootstrap failed'
1064 # remove the oneshot script
1065 rm -f "$mpt/root/munge-it.sh"
1066
1067 #######################
1068 # FINISH AND CLEAN UP #
1069 #######################
1070
1071 w --infobox 'OK. We will now clean up the target system.' 7 72
1072
1073 # to minimise size of backing sparse image file (also good for SSD)
1074 fstrim -v "$mpt/boot/firmware"
1075 fstrim -v "$mpt"
1076 # add another couple of random bytes, so the first boot isn’t without
1077 dd if=/dev/urandom bs=64 count=1 conv=notrunc of="$mpt$rnd" || \
1078     p 'W: dd rnd3 failed'
1079 # that’s it
1080 p "I: done installing on $dvname ($tgtimg), cleaning up…"
1081 diecleanup
1082 set +x
1083 trap - EXIT
1084 # Debian #801614
1085 p 'W: when installing X11, you’ll need these extra steps:' \
1086     'N: 1. install the package xserver-xorg-legacy' \
1087     'N: 2. add to /etc/X11/Xwrapper.config the line:' \
1088     'N:     needs_root_rights=yes'
1089 p 'I: installation finished successfully'
1090 exit 0