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