#!/bin/bash
#
#
# Manage, customize, or burn an SSD or SD Card from a RasPiOS image
#
# Useful for maintaining one or more Pis. The general idea is to keep a "golden" image
# fully configured to suit your needs. Then, whenever you need a new SSD or SD Card (testing, new Pi, etc),
# start with the golden master. Also, when new RasPiOS releases come out, sdm can be used
# to easily build a customized fresh image for the new release.
#
# This script provides the infrastructure for your customization scripts to work
# in a repeatable manner against an official RasPiOS IMG file. 
#
# sdm [switches] image-name
# sdm --help will provide help on all available switches
#
# RPi Image management phases. See README.md for details
# Phase   Tasks
#     0   Copy files into unzipped IMG file (sdm calls phase0 script)
#         IMG file mounted so files can easily be copied into the IMG file
#     1   Nspawn into image to execute in the image context (via sdm)
#         APT packages can be installed in phase 1 so available on every SD card
#         Your custom script can be as automatic or manual as you choose
#         See sdm-customphase for an example
#     2   Write SSD or SD card from the IMG file (via sdm --burn)
#         Target system name is inserted onto the device after it's written
#     3   Boot the newly-created storage device
#

function ctrlcexit() {
    [ "$icolors" == "1" ] && resetcolors xt
    ferrexit "%% Exiting...\n"
}

function doexit() {
    trap "doexit" SIGINT
    [ "$icolors" == "1" ] && resetcolors xt
    docleanup
    exit
}

function write_premsg() {
    #
    # $1 has the message to write
    # It's written to the terminal and added to burnmsg
    #
    cmsgs+=("$1")
    #echo "$1"
    return 0
}

function extendandresize() {
    #
    # IMG must not be mounted
    # Leaves IMG mounted on completion
    #
    local extbytes=$((imgext*1024*1024)) loopdev

    echo "* Extend $dimgdevname by ${imgext}MB $(getgbstr $extbytes)..."
    extendimage "$dimg" "$imgext"
    #
    # Mount the image file into a loop device and resize the file system
    #
    echo "* Remount $dimgdevname to resize the file system"
    [ "$SDMPT" == "" ] && declare -x SDMPT=$(makemtpt)
    domount "$dimg" $dimgdevname
    echo "* Resize the file system"
    echo "% (Ignore 'on-line resizing required' message)"
    loopdev=$(getloopdev $SDMPT)
    resize2fs ${loopdev}
}    

function poptcheck() {
    #
    # Check options settings against valid settings
    # Report errors and exit
    #
    local popt="$1" vopt="$2" switchname="$3" badopt="" nopt="" os xopt=()
    if [ "$popt" != "" ]
    then
	IFS="," read -a xopt <<< "$popt"
	for os in "${xopt[@]}"
	do
	    if ! [[ "$vopt" =~ |$os| ]]
	    then
		[ "$badopt" != "" ] && badopt="$badopt, '$os'" || badopt="'$os'"
	    else
		nopt="$nopt|$os"
	    fi
	done
	[ "$badopt" != "" ] && echo "? Unrecognized $switchname value(s) $badopt" || echo "$nopt|"
    else
	echo ""
    fi
}

function printhelp() {
    echo $"sdm $version
Usage:
 sudo /usr/local/sdm/sdm --customize [switches] sd-image-file
   Customize an SD Card image or SSD/SD Card
 sudo $0 --explore sd-image-file
   Explore an SD Card image or SSD/SD Card
 sudo $0 --extend sd-image-file
   Extend an SD Card image without any customization
 sudo $0 --burn /dev/sdx --host target-hostname sd-image-file
   Burn the SD Card image to the burn device
 sudo $0 --shrink sd-image-file
   Shrink sd-image-file to the smallest possible size
 sudo $0 --mount sd-image-file
   Mount an SD Card image or an SSD/SD Card
 sudo $0 --aptmaint aptfunction sd-image-file
   Do APT maintenance (update, upgrade) on an SD Card image or SSD/SD Card
 sudo $0 --ppart sd-image-file
   Print partition tables in sd-image-file

Commands
 --burn devname      Copy the image to the storage device
 --burnfile filename Create a ready-to-burn customized Image file
 --customize         Customize the specified Image file
 --explore           Explore (nspawn shell) into image
 --info what         Display list of Locales, Keymaps, WiFi Countries, or Timezones
 --mount             Mount IMG file partitions and drop into interactive bash shell
 --ppart             Print partition tables in an IMG
 --shrink            Shrink an IMG file 
Command Switches for --customize and --burn or as noted
 --1piboot conf-file Use alternate 1piboot.conf
 --apt-dist-upgrade  Use apt-get dist-upgrade instead of upgrade 
 --aptcache IPADDR   Use apt-cacher-ng with server 'IPADDR'
 --aptmaint options  Do apt commands batch-mode in the image (update, upgrade, autoremove)
 --apt-options str   Control which apt functions are done (D: all) Values: noupdate,noupgrade,noautoremove,none
 --autologin         Auto-login to the console (Lite) or graphical desktop session
 --batch             Perform customize operation and exit
 --b0script script   Script to run after burn has completed
 --b1script script   Like --b0script, but done in nspawn (can do both b0script and b1script)
 --bootscripts       Run the scripts /usr/local/sdm/1piboot/0*-*.sh during first boot
 --bupdate plugin    Check and update sdm plugins on burned output; See documentation for details
 --burn-plugin pname:\"args\" Run burn plugin pname post-burn with the specified args (separate args with '|')
 --chroot            Use chroot rather than systemd-nspawn (for use on certain host OSes)
 --convert-root fmt  Use the specified rootfs format (btrfs, lvm) instead of ext4
 --encrypted         The rootfs is encrypted and must be decrypted by sdm
 --cscript script    Custom Phase Configuration script
 --csrc dir          Source directory passed for Custom Phase scripts
 --custom[1-4] str   Can be used in Custom cscripts
 --datefmt str       Date format for logs [%Y-%m-%d %H:%M:%S]
 --ddsw str          Switches for dd command [bs=16M iflag=direct]
 --debug apt         Enable debug features. apt=do single apt install for each package list (apps)
 --directory         sd-image-file is a directory tree rather than an IMG
 --domain name       Domain name (for use in Custom Phase Script; sdm does not use)
 --ecolors fg:bg:cur Set fg/bg/cursor colors when operating in the mounted IMG
 --expand-root       Expand the root partition after burning it to a device
 --extend            Extend the image by --xmb N MB [Default: 2048/2GB]
 --groups list,of,groups Use this list of groups as the default for the user plugin
                     [dialout,cdrom,floppy,audio,video,plugdev,users,adm,sudo,users,input,netdev,spi,i2c,gpio]
 --host hostname     Hostname to write onto the storage device with --burn
 --key-file kf       Specify a key-file for certain commands
 --loadlocal args    Load WiFi Credentials from USB stick during first boot (see README)
 --logwidth N        Split log lines longer than N characters [Default: 96]
 --mcolors fg:bg:cur Set fg/bg/cursor colors when operating in --mount
 --no-expand-root    Don't expand root partition and don't let RasPiOS do it either
 --nowait-timesync   Don't wait for time to sync in sdm-firstboot
 --nspawnsw str      Additional switches for systemd-nspawn
 --oklive            It's OK to run plugins on running system without notification
 --plugin pname:\"args\" Run plugin pname with the specified args (separate args with '|')
 --plugin-debug      Print plugin debug messages
 --norestart         Do not restart after first boot (use on --burn command)
 --os osname         Specify OS in the Image (D:raspios, ubuntu)
 --reboot n          n seconds after first boot has completed restart the system
 --redact            Redact passwords in /etc/sdm/cparams and /etc/sdm/history
 --redo-customize    If image already customized, redo without prompting
 --regen-ssh-host-keys Regenerate system SSH keys at end of FirstBoot
 --restart           20 seconds after first boot has completed restart the system
 --runonly plugins   Do no customization, ony run requested plugins
 --sdmdir /path/to/sdm Put sdm here instead of /usr/local/sdm when customizing an IMG
 --showapt           Display apt output as well as logging it
 --xmb n             Set the --extend size in MB [2048]
 --version           Print sdm version number"
}

#
# Initialize and Parse the command
#
#
version="V13.4"
aptcache=""                 #IP address of apt-cacher-ng server
aptdistupgrade=0            #1=Use apt-get dist-upgrade instead of upgrade
aptmaint=""                 #--aptmaint switch values
aptfunction=0               #1=Some apt batch function specified
autologin=0                 #1=auto-login to console or desktop
b0script=""                 #Customization script to run after burn
b1script=""                 #Like b0script but done in nspawn
bootscripts=0               #Run FirstBoot custom boot scripts
bupdate=""                  #--bupdate options selected
burn=0                      #1=Burn the image to the SD card
burndev=""                  #SD card device name for --burn
burnfile=0                  #1=Burn image to a file
burnfilefile=""             #Filename for --burnfile
burnplugins=""              #Burn-time plugin list
csrc=""                     #Source directory for use by Custom Phase script
cscript=""                  #Custom Phase script
cvtrootfs=""                #--convert-root
datefmt="%Y-%m-%d %H:%M:%S" #Default date format for history log
ddpid=""                    #Holds pid of dd during IMG Extend
ddextend=0                  #1=use dd for extend instead of qemu-img
ddsw="bs=16M iflag=direct"  #Switches for dd
debugs=""                   #Debug settings
dgroups="dialout,cdrom,floppy,audio,video,plugdev,users,adm,sudo,users,input,netdev,spi,i2c,gpio" #Add created users to these groups
dimg=""                     #IMG Name
dimgdevname=""              #Set to "IMG", "Device", or "Directory" once determined
domain=""                   #--domain name
ecolors="blue:gray:red"     #fg:bg:cursor
expandroot=0                #1=Expand root after burning it to a device
fchroot=0                   #1=Use chroot instead of systemd-nspawn
fcustomize=0                #True if customizing an image (either --customize or lack of burn,mount,explore)
fbatch=0                    #1=nspawn "batch" mode (non-interactive). Do Phase1 and exit
fdirtree=0                  #1=source is a directory tree, not an IMG or device
fdomount=0                  #1=Do a mount and drop into bash
fencrypted=0                #1=rootfs is encrypted
fexplore=0                  #1=Just fire up nspawn to explore the system
fextend=0                   #1=extend image by --xmb MB
fextendonly=0               #1=Just extend and exit (this if no other command specified)
fgpt=0                      #1=Convert burned device to gpt format
fnoexpandroot=0             #1=Don't expand root partition and don't let RasPiOS do it either
fppart=0                    #1=Print partition tables in an IMG
frunonly=0                  #1=--runonly
fshrink=0                   #1=Shrink the IMG
hname=""                    #Host name when using --burn
hostname=""                 #Written to cparams during --burn
hotspot=""                  #WiFi config file
imgext=0                    #Number of MB to extend IMG
infocmd=0                   #1=--info command
keyfile=""                  #--key-file
loadl10n=0                  #1=Load Localization settings from running system 
loadlocal=""                #Non-null specifies loading wifi creds on firstboot (currently USB only)
logwidth=96                 #Split log lines longer than this
loopdev=""                  #Name of mounted loop device
mcolors="black:LightSalmon1:blue" #fg:bg:cursor
myuser=""                   #Non-root user to create. Default is no non-root user created
noreboot=0                  #1=Do not restart system after first boot
nowaittimesync=0            #1=Don't wait for time to sync in sdm-firstboot
oklive=0                    #1=--oklive don't notify about running system
os="raspios"                #Default OS for images
nspawnsw=""                 #Switches for nspawn
plugindebug=0               #1=Enable debugging output
plugins=""                  #List of plugins to include
pluglistlist=""             #List of plugin @listofplugins files
allplugins=""               #List of all plugins that have been run
poptions=""                 #Phase options
pvers=0                     #Print version number
raspiosver=""               #RasPiOS Debian version # (10:Buster, 11:Bullseye, 12:Bookworm)
fredact=0                   #1=redact passwords in /etc/sdm/{cparams,history}
reboot=0                    #1=Reboot from First Boot
rebootwait=20               #Number of seconds to wait after systemd thinks system is fully booted to reboot
regensshkeys=0              #1=Regenerate SSH keys at end of FirstBoot
runonly=""                  #Keyword specified for --runonly
drebootwait=$rebootwait     # Used to see if rebootwait should be modified on burn
redocustomize=0             #1=Don't prompt if image already customized
sdmdir="/usr/local/sdm"     #Where to put sdm when customizing. --sdmdir to change
sdmflist="sdm sdm-phase0 sdm-phase1 sdm-cparse sdm-readparams sdm-cmdsubs sdm-rpcsubs sdm-cryptconfig sdmcryptfs sdm-firstboot sdm-apt sdm-apt-cacher sdm-cportal sdm-logmsg sdm-gburn sdm-add-luks-key sdm-make-luks-usb-key sdm-collect-labwc-config"
swapsize=0                  #Set swap size to nMB (overrides --disable swap)
vaptmaintops="|update|upgrade|autoremove|" #Options for --apt
virtmode="nspawn"           #"nspawn" or "chroot"
vcvtrootfs="|btrfs|ext4|lvm|" #Valid --convert-root settings
vdebug="|all|apt|"          #Valid --debug settings
vpoptions="|noupdate|noupgrade|noautoremove|nologdates|none|" #Valid --poptions
#vloadopts="|usb|wifi|flashled|internet|"  #Valid options for --loadlocal
vloadopts="|wifi|"          #Valid options for --loadlocal
vosopts="|raspios|raspbian|ubuntu|"
vbupdopts="|plugin|plugins|sdm|" #Valid options for --bupdate
vrunonlyopts="|plugin|plugins|"  #Valid options for --runonly
showapt=0                   #1=Display apt output on terminal as well as logging
pi1bootconf=""              #Command-line specified 1piboot.conf file to use
custom1=""                  #For custom use
custom2=""                  #For custom use
custom3=""                  #For custom use
custom4=""                  #For custom use

src=$(dirname "$(realpath "$0")")
source $src/sdm-cparse           # Get function defs
[[ ! $EUID -eq 0 ]] && errexit "? Please run as root: sudo $0 $*"
#
# Parse the command line
#
cmdline="$0 $*"
longopts="help,1piboot:,apps:,aptcache:,apt-dist-upgrade,aptmaint:,apt-options:,\
autologin,b0script:,b1script:,batch,bootscripts,burn:,burnfile:,burn-plugin:,chroot,cscript:,convert-root:,csrc:,\
customize,datefmt:,ddextend,ddsw:,debug:,directory,domain:,ecolors:,encrypted,explore,expand-root,extend,\
gpt,groups:,host:,hostname:,info,keyfile:,loadlocal:,locale:,logwidth:,\
mcolors:,mount,no-expand-root,norestart,noreboot,nowait-timesync,nspawnsw:,oklive,os:,plugin-debug,\
plugin:,plugins:,poptions:,ppart,reboot:,redact,redo-customize,\
regen-ssh-host-keys,restart,runonly:,sdmdir:,showapt,shrink,swap:,\
bupdate:,xmb:,custom1:,custom2:,custom3:,custom4:,version"

OARGS=$(getopt -o h --longoptions $longopts -n 'sdm' -- "$@")
[ $? -ne 0 ] && errexit "? $0: Unable to parse command"
eval set -- "$OARGS"

while true
do
    case "${1,,}" in
	# 'shift 2' if switch has argument, else just 'shift'
	--1piboot)     pi1bootconf=$2; shift 2 ;;
	--aptcache)    aptcache=$2   ; shift 2 ;;
	--apt-dist-upgrade) aptdistupgrade=1 ; shift 1 ;;
	--aptmaint)    aptmaint="${2,,}"; shift 2 ;;
	--autologin)   autologin=1   ; shift 1 ;;
	--b0script)    b0script=$2   ; shift 2 ;;
	--b1script)    b1script=$2   ; shift 2 ;;
	--batch)       fbatch=1      ; shift 1 ;;
	--bootscripts) bootscripts=1 ; shift 1 ;;
	--burn)        burn=1 ;
		       burndev=$2    ; shift 2 ;;
	--burnfile)    burnfile=1 ;
		       burnfilefile=$2 ; shift 2 ;;
	--burn-plugin) burnplugins=$(appendvalue "$burnplugins" "$2" "~") ; shift 2 ;;
	--chroot)      fchroot=1     ; shift 1 ;;
	--convert-root) cvtrootfs="${2,,}" ; shift 2 ;;
	--cscript)     cscript=$2    ; shift 2 ;;
	--csrc)        csrc=$2       ; shift 2 ;;
	--customize)   fcustomize=1  ; shift 1 ;;
	--datefmt)     datefmt=$2    ; shift 2 ;;
	--ddextend)    ddextend=1    ; shift 1 ;;
	--ddsw)        ddsw=$2       ; shift 2 ;;
	--debug)       debugs=$2     ; shift 2 ;;
        --directory)   fdirtree=1    ; shift 1 ;;
	--domain)      domain=$2     ; shift 2 ;;
	--ecolors)     ecolors=$2    ; shift 2 ;;
	--encrypted)   fencrypted=1  ; shift 1 ;;
	--expand-root) expandroot=1  ; shift 1 ;;
	--explore)     fexplore=1    ; shift 1 ;;
	--extend)      fextend=1     ; shift 1 ;;
	--gpt)         fgpt=1        ; shift 1 ;;
	--groups)      dgroups=$2    ; shift 2 ;;
	--hostname|--host) hname=$2  ; shift 2 ;;
	--info)        infocmd=1     ; shift 1 ;;
	--keyfile)     keyfile=$2    ; shift 2 ;;
	--loadlocal)   loadlocal="${2,,}" ; shift 2 ;;
	--logwidth)    logwidth=$2   ; shift 2 ;;
	--mcolors)     mcolors=$2    ; shift 2 ;;
	--mount)       fdomount=1    ; shift 1 ;;
	--no-expand-root) fnoexpandroot=1 ; shift 1 ;;
	--norestart|--noreboot) noreboot=1 ; shift 1 ;;
	--nowait-timesync) nowaittimesync=1 ; shift 1 ;;
	--nspawnsw)    nspawnsw=$2   ; shift 2 ;;
	--oklive)      oklive=1      ; shift 1 ;;
	--os)          os="${2,,}"   ; shift 2 ;;
	--plugin-debug) plugindebug=1 ; shift 1 ;;
	--plugin|--plugins) plugins=$(appendvalue "$plugins" "$2" "~") ; shift 2 ;;
	--apt-options|--poptions)    poptions=$(appendvalue "$poptions" "${2,,}" ",") ; shift 2 ;;
	--ppart)       fppart=1      ; shift 1 ;;
	--redact)      fredact=1     ; shift 1 ;;
	--reboot)      rebootwait=$2 ;
		       reboot=1      ; shift 2 ;;
	--redo-customize) redocustomize=1 ; shift 1 ;;
	--regen-ssh-host-keys) regensshkeys=1  ; shift 1 ;;
	--restart)     reboot=1      ; shift 1 ;;
	--runonly)     runonly="${2,,}" ; shift 2 ;;
	--sdmdir)      sdmdir=$2     ; shift 2 ;;
	--showapt)     showapt=1     ; shift 1 ;;
	--shrink)      fshrink=1     ; shift 1 ;;
	--swap)        swapsize=$2   ; shift 2 ;;
	--bupdate)     bupdate=$(appendvalue "$bupdate" "$2" "|") ; shift 2 ;;
	--xmb)         imgext=$2     ; shift 2 ;;
	--custom1)     custom1=$2    ; shift 2 ;;
	--custom2)     custom2=$2    ; shift 2 ;;
	--custom3)     custom3=$2    ; shift 2 ;;
	--custom4)     custom4=$2    ; shift 2 ;;
	--version)     pvers=1       ; shift 1 ;;
	--)            shift ; break ;;
	-h|--help)     printhelp ; shift ; exit ;;
	*) errexit "? $0: Internal error" ;;
    esac
done

dimg="$1"
[ $pvers -eq 1 ] && echo "sdm $version" && exit 0

#
# Adjust settings based on switches and check for conflicting switches
# and erroneous switch values
#

if [ "$cscript" != "" ]
then
    if [ ! -x "$cscript" ]
    then
	fn="$src/$(basename $cscript)"
	if [ -x "$fn" ]
	then
	    cscript=$fn
	fi
    fi
fi
#
# Handle --info switch right now. $dimg has the requested info item (timezones, locales, keymaps, wifi-countries)
#
if [ $infocmd -eq 1 ]
then
    case "${dimg,,}" in
	time*)
	    less /usr/share/zoneinfo/zone1970.tab ;;
	local*)
	    less /usr/share/i18n/SUPPORTED ;;
	key*)
	    less /usr/share/doc/keyboard-configuration/xorg.lst ;;
	wifi*)
	    less /usr/share/zoneinfo/iso3166.tab ;;
	help|*)
	    [ "${dimg,,}" != "help" ] && echo "? Unrecognized --info option '$dimg'" ;
	    echo $"
The --info command accepts one of four switch values:
timezone:     Show --timezone values
locale:       Show --locale values
keymap:       Show --keymap values
wifi-country: Show --wifi-country values

Keys can be abbreviated to 'time', 'local', 'key', and 'wifi'"
	    ;;
    esac
exit 0
fi
[ "${dimg,,}" == "help" ] && printhelp && exit
declare -a cmsgs                    # In case customization messages generated before we're ready to logtoboth them
cscript="$(fndotfullpath $cscript)"
[[ $burn -eq 1 ]] && [[ $burnfile -eq 1 ]] && errexit "? Switch conflict: --burn and --burnfile"
[[ $burn -eq 1 ]] || [[ $burnfile -eq 1 ]] && burning=1 || burning=0
[[ $burning -eq 1 ]] && [[ $fdomount -eq 1 ]] && errexit "? Switch conflict: --burn|--burnfile and --mount"
[[ $burning -eq 1 ]] && [[ $fexplore -eq 1 ]] && errexit "? Switch conflict: --burn|--burnfile and --explore"
[[ $burning -eq 1 ]] && [[ $fshrink -eq 1 ]] && errexit "? Switch conflict: --burn|--burnfile and --shrink"
[[ $fdomount -eq 1 ]] && [[ $fexplore -eq 1 ]] && errexit "? Switch conflict: --mount and --explore"
[[ $fdomount -eq 1 ]] && [[ $fshrink -eq 1 ]] && errexit "? Switch conflict: --mount and --shrink"
[[ $reboot -eq 1 ]] && [[ $noreboot -eq 1 ]] && errexit "? Switch conflict: --restart and --norestart"
[[ $fcustomize -eq 1 ]] && [[ $burning -eq 1 ]] && errexit "? Switch conflict: --customize and --burn|--burnfile"
[[ $fcustomize -eq 1 ]] && [[ $fdomount -eq 1 ]] && errexit "? Switch conflict: --customize and --mount"
[[ $fcustomize -eq 1 ]] && [[ $fexplore -eq 1 ]] && errexit "? Switch conflict: --customize and --explore"
[[ $fcustomize -eq 1 ]] && [[ $fshrink -eq 1 ]] && errexit "? Switch conflict: --customize and --shrink"
[[ $burning -eq 1 ]] && [[ $fdirtree -eq 1 ]] && errexit "? Switch conflict: --directory and --burn|--burnfile"
[[ $fexplore -eq 1 ]] && [[ $fshrink -eq 1 ]] && errexit "? Switch conflict: --explore and --shrink"
[[ $fgpt -eq 1 ]] && [[ $burn -ne 1 ]] && errexit "? Switch conflict: --gpt only valid with --burn"
[[ "$cvtrootfs" != "" ]] && [[ $burning -ne 1 ]] && errexit "? Switch conflict: --convert-root only valid with --burn"
[[ $fppart -eq 1 ]] && [[ $((burning+fcustomize+fexplore+fdomount+fshrink)) -ne 0 ]] && errexit "? Switch conflict: --ppart and any of 'burn,burnfile,explore,mount,shrink'"
[[ $fencrypted -eq 1 ]] && [[ $((fdomount+fexplore)) -eq 0 ]] && errexit "? Switch conflict: --encrypted only valid with --explore and --mount"
if [ "$aptmaint" != "" ]
then
    [ $((burning+fcustomize+fexplore+fdomount+fshrink+fppart)) -gt 0 ] && errexit "? One or more switches conflict with --aptmaint"
    aptfunction=1
fi
if [ "$runonly" != "" ]
then
    [ $((burning+fcustomize+fexplore+fdomount+fshrink+fppart)) -gt 0 ] && errexit "? One or more switches conflict with --runonly"
    frunonly=1
    [ "$dimg" == "" ] && dimg="/" && fdirtree=1  # Default to running system if nothing specified
fi
#
# Ensure action requested: aptmaint, burn, customize, explore, mount, shrink, ppart 
#
if [ $((aptfunction+burning+fcustomize+fexplore+fdomount+fshrink+fppart+frunonly)) -eq 0 ]
then
    # Handle --extend only 
    [ $fextend -eq 0 ] && errexit "? No command specified (--aptmaint --burn, --customize, --explore, --mount, --runonly)"
    fextendonly=1
fi
[[ "$src" != "" ]] && [[ ! -d "$src" ]] && errexit "? sdm source directory '$src' not found"
[[ "$csrc" != "" ]] && [[ ! -d "$csrc" ]] && errexit "? Custom source directory '$csrc' not found"
[[ "$cscript" != "" ]] && [[ ! -f "$cscript" ]] && errexit "? Custom Phase Script '$cscript' not found"
[[ "$cscript" != "" ]] && [[ ! -x "$cscript" ]]  && errexit "? Custom Phase Script '$cscript' not executable"
pi1bootconf="$(fndotfullpath $pi1bootconf)"
[[ "$pi1bootconf" != "" ]] && [[ ! -f "$pi1bootconf" ]] && errexit "? Custom 1piboot.conf file '$pi1bootconf' not found"
[ "$rebootwait" != "" ] && checknumeric "$rebootwait" "--reboot"
[ "$imgext" != "" ] && checknumeric "$imgext" "--xmb"
#! ckwificountry $wificountry && errexit "? Unknown WiFi Country '$wificountry'"

cvtrootfs=$(poptcheck "$cvtrootfs" "$vcvtrootfs" "--convert-root")
exitiferr "$cvtrootfs"
cvtrootfs="${cvtrootfs//|/}"  # Remove vbars: single value switch
debugs=$(poptcheck "$debugs" "$vdebug" "--debug")
exitiferr "$debugs"
poptions=$(poptcheck "$poptions" "$vpoptions" "--apt-options|--poptions")
exitiferr "$poptions"
[[ "$poptions" =~ "none" ]] && poptions="${poptions/none/noupdate|noupgrade|noautoremove}"
loadlocal=$(poptcheck "$loadlocal" "$vloadopts" "--loadlocal")
exitiferr "$loadlocal"
aptmaint=$(poptcheck "$aptmaint" "$vaptmaintops" "--aptmaint")
exitiferr "$aptmaint"
bupdate=$(poptcheck "$bupdate" "$vbupdopts" "--bupdate")
exitiferr "$bupdate"
runonly=$(poptcheck "$runonly" "$vrunonlyopts" "--runonly")
exitiferr "$runonly"

if [ $fexplore -eq 1 ]
then
    [ "$keyfile" != "" ] && fencrypted=1
fi

if [ "$runonly" != "" ]
then
    [[ "$plugins" == "" ]] && [[ "$burnplugins" == "" ]] && errexit "? No plugins specified for --runonly plugins"
fi
os=$(poptcheck "$os" "$vosopts" "--os")
exitiferr "$os"
os=${os#|} ; os=${os%|}  #Strip off the vbars
[[ "$os" == "raspbian" ]] || [[ "$os" == "" ]] && os="raspios"  #Allow old habits to never die ;)
[ "$os" != "raspios" ] && errexit "? Unsupported --os value: $os"

if [ "$plugins" != "" ]
then
    splugins="$plugins"
    plugins=""
    IFS="~" read -a plugs <<< $splugins
    for p in "${plugs[@]}"
    do
	if [ "${p:0:1}" == "@" ]
	then
	    pluglist=${p:1:999}
	    if [ -f "$pluglist" ]
	    then
		while read line
		do
		    line=$(stripbcline "$line")
		    if [ "$line" != "" ]
		    then
			IFS=":" read -r p pargs <<< "$line"
			# Strip quotes surrounding the arg list
			pargs=$(stripquotes "$pargs")
			[ "$pargs" == "$p" ] && pargs=""      #Really a null argument list
			[ "$pargs" == "" ] && line=$p || line="${p}:${pargs}"
			plugins=$(appendvalue "$plugins" "$line" "~")
		    fi
		done < "$pluglist"
		pluglistlist=$(appendvalue "$pluglistlist" "$pluglist" "~")
	    else
		errexit "? Plugin list file '$pluglist' not found"
	    fi
	else
	    plugins=$(appendvalue "$plugins" "$p" "~")
	fi
    done
fi

checkpluginsx "$plugins" y
checkpluginsx "$burnplugins" n

[[ $fextend -eq 1 ]] && [[ "$(type -p parted)" == "" ]] && errexit "? Cannot find parted; Do 'sudo apt install parted' and then rerun sdm"
[[ $cvtrootfs != "" ]] && [[ "$(type -p sgdisk)" == "" ]] && errexit "? Cannot find sgdisk; Do 'sudo apt install gdisk' and then rerun sdm"
#
# Check sanity of the disk image argument
#
dimgdevname="IMG"
[ "$dimg" == "" ] && errexit "? No disk image specified"
dimgdev=0
if [ $fdirtree -eq 1 ]
then
    [ ! -d $dimg ] && errexit "? Cannot find directory '$dimg'"
    dimgdevname="Directory"
elif [ ! -f "$dimg" ]
then
    p1=$(getpartname $dimg 1)
    [ ! -b ${dimg}${p1} ] && errexit "? Disk image file or device '$dimg' does not exist"
    dimgdev=1
    dimgdevname="Device"
fi    
if [[ $dimgdev -eq 0 ]] && [[ -f "$dimg" ]]
then
    for fext in xz XZ zip ZIP
    do
	if [ "${dimg%%.$fext}" != "$dimg" ]
	then
	    errexit "? File '$dimg' appears to be a ${fext^^} file\n  un${fext,,} it to operate on the IMG contained within it"
	fi
    done
elif [ $fextend -eq 1 ]
then
    echo "% Ignoring --extend for non-file-based $dimgdevname '$dimg'" && fextend=0
fi

if [ $fencrypted -eq 1 ]
then
    [ "$dimgdevname" != "Device" ] && errexit "? --encrypted only valid on disk devices"
    [ "$(type -p cryptsetup)" == "" ] && errexit "? Cannot find cryptsetup for --encrypted; Do 'sudo apt install cryptsetup' and rerun sdm"
fi

if [ "$b0script" != "" ]
then
    [ ! -f $b0script ] && errexit "? --b0script file '$b0script' not found"
fi
if [ "$b1script" != "" ]
then
    [ ! -f $b1script ] && errexit "? --b1script file '$b1script' not found"
fi
#
# parse and set ecolors
#
[ "$ecolors" == "" ] && ecolors="blue:gray:red"
if [ "$ecolors" != "0" ]
then
    IFS=":" read efg ebg ecursor <<< $ecolors
    [ "$efg" == "" ] && efg="blue"
    [ "$ebg" == "" ] && ebg="gray"
    [ "$ecursor" == "" ] && ecursor="red"
    ecolors="$efg:$ebg:$ecursor"
fi
#
# Process the command. Actions are burn, customize, mount, runonly, and standalone --extend
#
trap "doexit" EXIT
thishost="$(hostname)"
#
# Process --burn command
#
if [ $burning -eq 1 ]
then
    if iswsl
    then
	 [ $burnfile -eq 0 ] && errexit "? sdm burn not supported on Windows WSL"
    fi
    case "$cvtrootfs" in
	btrfs)
	    [ "$(type -p btrfs)" == "" ] && errexit "? Cannot find btrfs; Do 'sudo apt install btrfs-progs' and then rerun sdm"
	    ;;
	lvm)
	    [ "$(type -p pvs)" == "" ] && errexit  "? Cannot find lvm; Do 'sudo apt install lvm2' and then rerun sdm"
	    [ $fgpt -eq 0 ] && fgpt=1 && echo "% Force --gpt for --convert-root lvm"
	    ;;
	zfs)
	    [ "$(type -p zfs)" == "" ] && errexit "? Cannot find zfs; Follow the Disks-Partitions sdm doc (zfs section) and then rerun sdm"
	    [ $fgpt -eq 0 ] && fgpt=1 && echo "% Force --gpt for --convert-root zfs"
	    ;;
    esac
    source $src/sdm-cmdsubs
    sdm_burndevfile
    [ $burn -eq 1 ] && echo "* Storage '$burndev' is ready" || echo "* Storage Image '$burnfilefile' is ready"
    exit
fi
#
# Process --shrink command
#
if [ $fshrink -eq 1 ]
then
    declare -x SDMPT=$(makemtpt)
    source $src/sdm-cmdsubs
    sdm_shrink
    exit
fi
if [ $fppart -eq 1 ]
then
    source $src/sdm-cmdsubs
    sdm_ppart
    exit
fi
#
# Process --mount command
#
if [ $fdomount -eq 1 ]
then
    declare -x SDMPT=$(makemtpt)
    domount "$dimg" $dimgdevname
    echo "* $dimgdevname '$dimg' mounted on $SDMPT"
    if [ -t 0 ]
    then
	echo $"** BE VERY CAREFUL!! **
** Precede all path references by '$SDMPT' or you WILL modify your running system **
** Use 'exit' to Exit the bash shell and unmount the $dimgdevname"
	if [ "$mcolors" != "0" ]
	then
	    IFS=":" read mfg mbg mcursor <<< $mcolors
	    [ "$mfg" == "" ] && mfg="black"
	    [ "$mbg" == "" ] && mbg="LightSalmon1"
	    [ "$mcursor" == "" ] && mcursor="blue"
	    stermcolors "$mfg" "$mbg" "$mcursor" xt
	fi
    fi
    cd $SDMPT
    bash
    cd - > /dev/null
    [ -t 0 ] && [ "$mcolors" != "0" ] && resetcolors xt
    exit 0
fi
#
# Extend the image if --extend
#
if [ $fextendonly -eq 1 ]
then
    [ $imgext -eq 0 ] && imgext=2048
    if [ $fextend -eq 1 ] # Only on if file-based (e.g., can be extended)
    then
	extendandresize
    fi
    exit 0
fi
#
# IMG architecture not detected before this point so don't yet know if chroot
#
#
# Handle --runonly plugins
#
if [ $frunonly -eq 1 ]
then
    if [ "$plugins" != "" ]
    then
	if [[ $fdirtree -eq 1 ]] && [[ "$dimg" == "/" ]]
	then
	    if [ $oklive -eq 0 ]
	    then
		if israspios
		then
		    askyn "** Do you really want to run plugins live on the running host RasPiOS system" || exit 1
		else
		    errexit "? Host OS is not RasPiOS. Use --oklive to run plugins live on this system"
		fi
	    fi
	    # Either --oklive or said yes to modify running system
	    live0scripts "/etc/sdm/0piboot/*.sh" clear
	    logtoboth "* Run selected plugins on the host system"
	    declare -x SDMPT=""
	    declare -x SDMNSPAWN="Live0"
	    runplugins_exit "$plugins" 0
	    declare -x SDMNSPAWN="Live1"
	    runplugins_exit "$plugins" 1
	    runplugins_exit "$plugins" post-install
	    live0scripts "/etc/sdm/0piboot/*.sh" run
	else
	    # Operating on an IMG or disk
	    declare -x SDMPT=$(makemtpt)
	    domount "$dimg" $dimgdevname
	    initvirt write_premsg
	    [ "$bupdate" != "" ] && checkupdsdm update "$bupdate" || checkupdsdm check "$bupdate"
	    runplugins_exit "$plugins" 0
	    sdm_runspawncmd Phase1 run-plugin-list 1 "$plugins" || exit
	    sdm_runspawncmd Phase1 run-plugin-list post-install "$plugins" || exit
	    printnotes
	fi
	exit 0
    elif [ "$burnplugins" != "" ]
    then
	logtoboth "* Run selected burn plugins on $dimgdevname '$dimg'"
	[ "$dimgdevname" == "Device" ] && burndev=$dimg || burndev=""
	[ "$dimgdevname" == "IMG" ] && burnfilefile=$dimg || burnfilefile=""
	runplugins_exit "$burnplugins" burn-complete "burndev=$burndev|burnfilefile=$burnfilefile|imgtype=$dimgdevname"
	exit 0
    fi
fi
# Not --extend (only). Other steps need IMG mounted
declare -x SDMPT=$(makemtpt)
domount "$dimg" $dimgdevname
[ $fexplore -eq 0 ] && initvirt write_premsg || initvirt echo
#
# Handle commands --aptmaint, --customize, and --explore
#
sdmdone="$SDMPT/etc/sdm/custom.ized"
icolors=0    # Don't set colors around systemd-nspawn
if [ $fcustomize -eq 0 ]
then
    if [ "$aptmaint" != "" ]
    then
	fbatch=1
	spawncmd="$sdmdir/sdm-phase1 apt $aptmaint"
	if [ ! -f $SDMPT/$sdmdir/sdm-phase1 ]
	then
	    errexit "? sdm has not customized $dimgdevname '$dimg'"
	fi
    else
	#
	# Doing --explore
	#
	echo "* Enter $dimgdevname '$dimg'"
	spawncmd=""
	icolors=1
    fi
else
    #
    # Doing a customization
    # Create and populate $sdmdir and /etc/sdm in the IMG
    #
    if [[ -f $sdmdone ]] && [[ $redocustomize -eq 0 ]]
    then
	if ! askyn "** $dimgdevname '$dimg' has already been customized. Redo?" "-n 1"
	then
	    echo ""
	    echo "** Explore the image with '$0 --explore $dimg'"
	    exit 0
	else
	    echo ""
	fi
    fi
    customizestart="$(getcdate)"
    #
    # Extend image if requested
    #
    if [ $fextend -eq 1 ]
    then
	echo "* Unmount $dimgdevname for --extend"
	docleanup keep
	[ $imgext -eq 0 ] && imgext=2048
	extendandresize   # Remounts IMG afterward
    fi
    pi1bootconf="$(fndotfullpath $pi1bootconf)"
    spawncmd="$sdmdir/sdm-phase1"
    #
    # Create and populate $sdmdir tree
    #
    [ ! -d $SDMPT/etc/sdm ] && mkdir -p $SDMPT/etc/sdm/assets $SDMPT/etc/sdm/0piboot $SDMPT/etc/sdm/xpiboot $SDMPT/etc/sdm/local-assets
    touch $SDMPT/etc/sdm/history
    chmod 700 $SDMPT/etc/sdm
    echo "# sdm added these settings from the command line (see /etc/sdm/history)" > $SDMPT/etc/sdm/auto-1piboot.conf
    mkdir -p $SDMPT/$sdmdir/1piboot
    for f in $sdmflist
    do
	cp -a $src/$f $SDMPT/$sdmdir
	setfileownmode $SDMPT/$sdmdir/$f 755
    done
    rm -f $SDMPT/usr/local/bin/sdm
    ln -s $sdmdir/sdm $SDMPT/usr/local/bin
    mkdir -p $SDMPT/$sdmdir/{plugins,local-plugins}
    if compgen -G "$src/plugins/*" > /dev/null
    then
	cp -a $src/plugins/* $SDMPT/$sdmdir/plugins
	setfileownmode "$SDMPT/$sdmdir/plugins/*" 755
    fi
    if compgen -G "$src/local-plugins/*" > /dev/null
    then
	cp -a $src/local-plugins/* $SDMPT/$sdmdir/local-plugins
	setfileownmode "$SDMPT/$sdmdir/local-plugins/*" 755
    fi
    # Always set this in case system with non-std location does sdm and sets it to /usr/local/sdm
    sed -i "s#SDMDIR=\".*\"#SDMDIR=\"$sdmdir\"#" $SDMPT/$sdmdir/sdm-gburn
    cp -a $src/sdm-readparams $SDMPT/etc/sdm
    setfileownmode $SDMPT/etc/sdm/sdm-readparams 755

    chown -R root:root $SDMPT/$sdmdir
    logtoboth "* Start Configuration"
    if compgen -G "$src/1piboot/*.sh" > /dev/null
    then
	cp -a -f $src/1piboot/*.sh $SDMPT/$sdmdir/1piboot
	setfileownmode $SDMPT/$sdmdir/1piboot/*.sh 755
    fi
    [ -e /sys/firmware/devicetree/base/model ] && pimodel=$(tr -d '\0' < /sys/firmware/devicetree/base/model) || pimodel=""
    if [ -e /proc/meminfo ]
    then
	memline=$(grep -i MemTotal /proc/meminfo)
	memsize="${memline##*:}"
	memsize="${memsize#"${memsize%%[![:space:]]*}"}"
    else
	memsize=""
    fi
    is64bit && hostarch="64-bit aarch64" || hostarch="32-bit armhf"
    raspiosver="$(getosinfo VERSION_ID $SDMPT)"
    logtoboth "> Command Line: $cmdline"
    logtoboth "* Host Information"
    logtoboth "   Hostname:  $(hostname)"
    [ "$memsize" != "" ] && logtoboth "   Memory:    $memsize"
    logtoboth "   uname:     $(uname -a)" nosplit
    [ "$pimodel" != "" ] && logtoboth "   Raspberry Pi Model: $pimodel"
    os_name="$(getosinfo PRETTY_NAME)"
    [ "$os_name" == "" ] && os_name="$(getosinfo NAME)"
    logtoboth "   os-release Name: $os_name"
    os_version="$(getosinfo VERSION)"
    os_codename="$(getosinfo VERSION_CODENAME)"
    if [ "$os_codename" != "" ]
    then
	[[ "$os_version" =~ $os_codename ]] || os_version="$os_version ($os_codename)"
    fi
    logtoboth "   Version: $os_version"
    os_like="$(getosinfo ID_LIKE)"
    if [ "$os_like" != "" ]
    then
	#[ "$os_like" != "debian" ] && logtoboth "   Like OS: $os_like"
	logtoboth "   Like OS: $os_like"
    fi
    logtoboth "   sdm Version: $version"
    logtoboth "* IMG Information"
    logtoboth "   Name: $dimg"
    logtoboth "   Date: $(getosdate)"
    logtoboth "   RasPiOS Version: $raspiosver"
    logtoboth "   RasPiOS Architecture: $hostarch"
    logtoboth "   os-release Version: $(getosinfo VERSION $SDMPT)"

    # Flush accumulated messages
    if [ ${#cmsgs[@]} -gt 0 ]
    then
        for (( i=0 ; i < ${#cmsgs[@]} ; i++ ))
        do
            logtoboth "${cmsgs[$i]}"
        done
    fi

    plugin_logorder "$plugins"

    logfreespace
    logtoboth "> Copy sdm to $sdmdir in the $dimgdevname"   # Yes, already done above ;)

    if [ "$pi1bootconf" != "" ]
    then
	logtoboth "> Copy Custom 1piboot.conf '$pi1bootconf' to the $dimgdevname"
	cp -a $pi1bootconf $SDMPT/etc/sdm/1piboot.conf
	setfileownmode $SDMPT/etc/sdm/1piboot.conf 644
	cp -a $pi1bootconf $SDMPT/$sdmdir/1piboot                                         #Drop a copy in $sdmdir in the IMG
	setfileownmode $SDMPT/$sdmdir/1piboot/$(basename $pi1bootconf) 644
    else
	if [[ -d $src/1piboot ]]  && [[ -f $src/1piboot/1piboot.conf ]]
	then
	    cp -a $src/1piboot/1piboot.conf $SDMPT/etc/sdm/
	    setfileownmode $SDMPT/etc/sdm/1piboot.conf 644
	fi
    fi
    # Also copy the original version into $sdmdir/1piboot
    cp -a $src/1piboot/1piboot.conf $SDMPT/$sdmdir/1piboot
    setfileownmode $SDMPT/$sdmdir/1piboot/1piboot.conf 644

    if [ "$cscript" != "" ]
    then
	logtoboth "> Copy Custom Phase Script '$cscript' to $sdmdir in the $dimgdevname"
	cp -a $cscript $SDMPT/$sdmdir
	setfileownmode $SDMPT/$sdmdir/$(basename $cscript) 755
    fi

    if [ "$pluglistlist" != "" ]
    then
	logtoboth "> Copy --plugin @file(s) to $SDMPT/etc/sdm/assets/pluglist"
	mkdir -p $SDMPT/etc/sdm/assets/pluglist
	IFS="~" read -a pfiles <<< "$pluglistlist"
	for pf in "${pfiles[@]}"
	do
	    logtoboth "  $pf"
	    cp -a $pf $SDMPT/etc/sdm/assets/pluglist
	done
    fi
    #
    # Write the config settings into the IMG
    #
    writeconfig
    #
    # Perform Phase 0 on the image
    #
    declare -x SDMNSPAWN="Phase0"
    $SDMPT/$sdmdir/sdm-phase0 || exit
    touch $sdmdone
    
    echo "* Enter image '$dimg' for Phase 1"
fi


[[ $icolors -eq 1 ]] && [[ -t 0 ]] && [[ "$spawncmd" == "" ]] || icolors=0
[[ "$ecolors" == "0" ]] || [[ $fbatch -eq 1 ]] && icolors=0
[ $icolors -eq 1 ] && stermcolors "$efg" "$ebg" "$ecursor" xt
[ "$spawncmd" == "" ] && spawncmd="/bin/bash"
sdm_spawn "$nspawnsw" Phase1 "$spawncmd"
sts=$?
[ $icolors -eq 1 ] && icolors=0 && resetcolors xt

exit $sts
