#!/bin/sh
# Remaster script - take a snapshot of the currently running system and create a new iso file
# Copyright (C) James Budiono 2012, 2013, 2014, 2018
# License: GNU GPL Version 3 or later
#
# Version 3 - minor changes to work with syslinux 6
# Version 2 - add EFI support
#
#131119 L18L internationalisation

# std localisation stanza
export TEXTDOMAIN=fatdog
. gettext.sh
# performance tweak - use "C" if there is no localisation
! [ -e $TEXTDOMAINDIR/${LANG%.*}/LC_MESSAGES/$TEXTDOMAIN.mo ] &&
! [ -e $TEXTDOMAINDIR/${LANG%_*}/LC_MESSAGES/$TEXTDOMAIN.mo ] && LANG=C

### configuration variables
# hardcoded configuration: "pup_init" and "kernel-modules"
APPTITLE="$(gettext 'Remaster Fatdog64')"
ISOLINUX_PATH=/usr/share/syslinux
ISOLINUX_BINS="isolinux.bin vesamenu.c32 libcom32.c32 libutil.c32 ldlinux.c32"
LOGO_FILE=/usr/share/backgrounds/fatdog-remastered.png
OUTPUT=remaster-$(date +%Y-%m-%d).iso
DEFAULT_MKSQUASHFS_OPTIONS="-comp xz"
MKSQUASHFS_OPTIONS=${MKSQUASHFS_OPTIONS:-$DEFAULT_MKSQUASHFS_OPTIONS}

. $BOOTSTATE_PATH   			# AUFS_ROOT_ID BASELINE_MOUNT MODULE_SFS_MOUNT BASE_SFS_PATH AUFS_ROOT
BASESFS=${BASE_SFS_PATH##*/}	# fd64-600.sfs

# modules required for small initrd 
BASE_MODULES="cryptoloop nbd cifs aufs usb-storage ohci uhci ehci xts dm-crypt dm-snapshot sg hid-generic" 

### runtime variables
layers=				# layers chosen for remaster
tmp_aufs=			# location of temparary aufs layer 
tmpfs=				# location of temporary tmpfs layer (this becomes first layer of aufs)
tmp_size=			# size requirement for location
savelocation=		# where the ISO will be saved
kernellocation=		# where to find the kernel
efibootlocation=	# where to find efiboot.img
small_initrd=no		# whether to split the initrd (yes, no)

### wizard step movements
step=1
prev_action=
prev_step=

go_step() { step=$1; }
go_next() { step=$((step + 1)); }
go_prev() { step=$((step - 1)); }
go_exit() { step=exit; }
next_wizard_step() {
	local action=$?
	[ "$1" ] && action=$1
	prev_step=$step
	case $action in
		0) go_next ;;				# Xdialog ok
		3) go_prev ;;				# Xdialog previous
		1|255) go_step cancel ;;	# Xdialog Esc/window kill
	esac
	prev_action=$action
}

# parameter: $1 - use $BACKTITLE
std_wizard_flags() {
	local flags="--title \"$APPTITLE\" --wizard --stdout --separator :"
	[ "$1" ] && flags=$flags" --backtitle \"$BACKTITLE\""
	echo $flags
}

### helper - taken from fatdog-split-initrd
### get BASE_MODULES with list of required modules and their dependencies
# $1-BASE_MODULES $2-MODULES_DEP output: returns BASE_MODULES
get_modules_dep() {
	local OLD_MODULES GREP_PARAM MODULES_DEP
	BASE_MODULES=$1 MODULES_DEP=$2
	while [ ! "$OLD_MODULES" = "$BASE_MODULES" ]; do
		OLD_MODULES=$BASE_MODULES
		get_grep_param $BASE_MODULES
		set -- $(grep -E "$GREP_PARAM" $MODULES_DEP)
		while [ $1 ]; do
			case $1 in
				*:) ;;
				*)	new=${1##*/}; new=${new%.ko}; 
					case $BASE_MODULES in 
						*$new*) ;;
						*) BASE_MODULES="$BASE_MODULES $new"
					esac ;;
			esac
			shift
		done
	done
}
# $*-modules, returns GREP_PARAM
get_grep_param() {
	GREP_PARAM=
	while [ $1 ]; do
		GREP_PARAM="$GREP_PARAM|/$1.ko:"
		shift
	done
	GREP_PARAM=${GREP_PARAM#|}
}

### helper functions
cleanup() {
	if [ "$tmp_aufs" ]; then
		umount $tmp_aufs
		umount $tmpfs
		rmdir $tmpfs $tmp_aufs
		tmpfs= && tmp_aufs=
	fi
}

### create a temporary aufs layers for remaster purpose
# parameter: $1 - all aufs layers to include
create_tmp_aufs() {
	local OIFS branches XPID
	
	OIFS=$IFS; IFS=:
	set -- $1
	IFS=$OIFS
	
	tmpfs=$(mktemp -dp /tmp remaster-tmpfs.XXXXXX)
	tmp_aufs=$(mktemp -dp /tmp remaster-aufs.XXXXXX)
	mount -t tmpfs tmpfs $tmpfs 
	chmod 0755 $tmpfs $tmp_aufs
	while [ $1 ];do
		branches=$branches:$1=ro+wh
		shift
	done
	branches=$tmpfs=rw$branches
	if ! mount -t aufs -o br=$branches aufs $tmp_aufs; then
		Xdialog --title "$APPTITLE" --infobox "$(gettext 'Cannot create temporary aufs mount, bailing out.')" 0 0 10000
		go_step exit
		return
	fi
	
	Xdialog --title "$APPTITLE" --no-buttons --infobox "$(gettext 'Calculating size requirement, please wait ...')" 3 60 1000000 &	
	XPID=$!
	tmp_size=$(du -sh $tmp_aufs | sed 's/\t.*//')
	kill $XPID
}

### map layers to locations - for display purposes
# parameter: $layer
map_layers_to_locations() {
	{ echo ==mount==; mount; 
	  echo ==losetup==; losetup-FULL -a; 
	  echo ==layers==; echo "$layers" | sed 's/:/\n/g'; } | awk '
	  /==mount==/ { mode=1 }
	  /==losetup==/ { mode=2 }
	  /==layers==/ { mode=3 }
	  {
		if (mode == 1) {
			# get list of mount points, types, and devices - index is $3 (mount points)
			mountdev[$3]=$1
			mounttypes[$3]=$5
		} else if (mode == 2) {
			# get list of loop devices and files - index is $1 (loop devs)
			sub(/:/,"",$1)
			sub(/.*\//,"",$3); sub(/)/,"",$3)
			loopdev[$1]=$3
		} else if (mode == 3) {
			# map mount types to loop files if mount devices is a loop
			for (m in mountdev) {
				if ( loopdev[mountdev[m]] != "" ) mounttypes[m]=loopdev[mountdev[m]]
			}
			# for (m in mountdev) print m " on " mountdev[m] " type " mounttypes[m]
			mode=4
		} else if (mode=4) {
			# print the branches and its mappings - except pup_init, which we hide
			printf("- %s - %s\n", $0, mounttypes[$0])
		}
	  }'
}

### wizard actions here
check_prerequisites() {
	if ! which mkisofs > /dev/null; then
		Xdialog --title "$APPTITLE" --infobox "$(gettext 'Missing mkisofs, cannot continue.')" 0 0 10000
		go_step exit
		return
	fi

	for p in $ISOLINUX_BINS; do
		if ! [ -f $ISOLINUX_PATH/$p ]; then
			Xdialog --title "$APPTITLE" --infobox "$(eval_gettext 'Missing $p, cannot continue.')" 0 0 10000
			go_step exit
			return
		fi
	done

	if ! [ "$MODULE_SFS_MOUNT" -a -e "$MODULE_SFS_MOUNT" ]; then
		Xdialog --title "$APPTITLE" --infobox "$(gettext 'Missing MODULE_SFS_MOUNT, cannot continue.')" 0 0 10000
		go_step exit
		return
	fi

	if ! [ "$BASELINE_MOUNT" -a -e "$BASELINE_MOUNT" ]; then
		Xdialog --title "$APPTITLE" --infobox "$(gettext 'Missing BASELINE_MOUNT, cannot continue.')" 0 0 10000
		go_step exit
		return
	fi
	go_next	
}

### blurb
introduction() {
	local msg=$(cat << EOF
$(gettext 'This application will create a new CD image that is a snapshot of your currently running system.') \
$(gettext 'It can capture everything --- your installed applications and your customised settings. \
You will have an option of what to include/exclude later on.') 

$(gettext 'The created CD image is an isohybrid, you can burn it to CD/DVD using Pburn \
or copied directly to USB flash drive using dd.')

$(gettext 'Press Yes to continue.')
EOF
)
	Xdialog --title "$APPTITLE" --fill --yesno "$msg" 0 0
	next_wizard_step
}

### confirm that user want to cancel
confirm_cancel() {
	local msg=$(cat <<EOF
$(gettext 'Are you sure you want cancel the remaster process?\n
Click Yes to really cancel and exit the program.')
EOF
)
	if Xdialog --title "$APPTITLE" --yesno "$msg" 0 0; then go_exit
	else go_step $prev_step
	fi
}

### get vmlinuz location
get_vmlinuz() {
	local BACKTITLE
	
	BACKTITLE="$(gettext 'Please tell me where I can find the kernel (vmlinuz).')\n\
$(gettext 'If you do not know where vmlinuz is, there is one on Live CD.')"
	kernellocation=$(eval "Xdialog $(std_wizard_flags 1) --no-buttons --fselect '' 0 0")
	next_wizard_step	
}

## get efiboot location
get_efiboot() {
	local BACKTITLE
	
	BACKTITLE="\
$(gettext 'If you do not need to support booting on EFI systems, click Next to continue.')\n\n
$(gettext 'Or else specify the location of EFI boot image (efiboot.img),\n
and tick the checkbox at the bottom.')\n\n
$(gettext 'If you do not know where efiboot.img is, there is on Live CD.')"

	efibootlocation=$(eval "Xdialog $(std_wizard_flags 1) --check \"$(gettext 'EFI boot support')\" off --no-buttons --fselect '' 0 0")
	next_wizard_step
	efibootlocation=$(echo "$efibootlocation" | tr '\n' '|')
	[ "${efibootlocation%|unchecked|}" == "$efibootlocation" ] && efibootlocation=${efibootlocation%|checked|} || efibootlocation=
	#echo $efibootlocation
}

### choose which layers to include
choose_layers() {
	local items msg 
	
	# 1. get aufs system-id for the root filesystem
	[ -z "$AUFS_ROOT_ID" ] && AUFS_ROOT_ID=$(
	awk '{ if ($2 == "/" && $3 == "aufs") { match($4,/si=[0-9a-f]*/); print "si_" substr($4,RSTART+3,RLENGTH-3) } }' /proc/mounts
	)

	# 2. get branches, then map branches to mount types or loop devices 
	items=$(
	{ echo ==mount==; cat /proc/mounts; 
	  echo ==losetup==; losetup-FULL -a; 
	  echo ==branches==; ls -v /sys/fs/aufs/$AUFS_ROOT_ID/br[0-9]* | xargs sed 's/=.*//'; } | awk '
	  /==mount==/ { mode=1 }
	  /==losetup==/ { mode=2 }
	  /==branches==/ { mode=3 }
	  {
		if (mode == 1) {
			# get list of mount points, types, and devices - index is $2 (mount points)
			mountdev[$2]=$1
			mounttypes[$2]=$3
		} else if (mode == 2) {
			# get list of loop devices and files - index is $1 (loop devs)
			sub(/:/,"",$1)
			sub(/.*\//,"",$3); sub(/)/,"",$3)
			loopdev[$1]=$3
		} else if (mode == 3) {
			# map mount types to loop files if mount devices is a loop
			for (m in mountdev) {
				if ( loopdev[mountdev[m]] != "" ) mounttypes[m]=loopdev[mountdev[m]]
			}
			# for (m in mountdev) print m " on " mountdev[m] " type " mounttypes[m]
			mode=4
		} else if (mode=4) {
			# print the branches and its mappings - except pup_init & kernel modules, which we hide
			if ($0 !~ /pup_init/ && $0 !~ /kernel-modules/) printf("%s %s on ", $0, mounttypes[$0])
		}
	  }  
	'
	)
	# ' this line to make Geany happy
	msg=$(cat <<EOF
$(gettext 'Choose layers you want to include in the remastered image.')
- pup_rw $(gettext '(if any) - is the RAM layer.')
- pup_save / devsave $(gettext '(if any) - is your currently active save file / device')
- pup_ro / devbase $(gettext '(if any) - is the base sfs file / device')
- $(gettext 'others') - $(gettext 'your currently used SFS')
$(gettext 'pup_rw and pup_save contains your customised settings and applications.')
EOF
)
	if layers=$(eval "Xdialog $(std_wizard_flags) --left --checklist \"$msg\" 0 0 7 $items"); then
		next_wizard_step
		cleanup
		create_tmp_aufs $layers
	else
		next_wizard_step
	fi
}

### choose where to save to
choose_save_location() {
	local BACKTITLE tmp_free_space
	
	tmp_free_space=$(df -h /tmp | awk 'NR==2 {print $4}')
	BACKTITLE="$(gettext 'Choose where to save the CD/DVD image output file (default is /tmp)')\n\n\
$(eval_gettext 'Output requires $tmp_size disk space. /tmp has $tmp_free_space free.')\n\
$(gettext 'If output can fit in /tmp, it is recommended to use it.')\n\
$(gettext 'If you use another location, make sure it is on a Linux filesystem.')"
	eval echo -e \""$BACKTITLE"\"
	savelocation=$(eval "Xdialog $(std_wizard_flags 1) --dselect '/tmp' 0 0")
	next_wizard_step	
}

### choose whether we want to split the initrd
choose_split() {
	local msg
	
	msg="$(gettext 'Choose the type of initrd you want to use.')
- $(gettext 'Standard initrd boots faster on harddisk frugal install, and is required for network boot.')
- $(gettext 'Small initrd boots faster on flash drives and CD/DVD.')"
	small_initrd=$(eval "Xdialog $(std_wizard_flags 1) --left --no-tags --radiolist \"$msg\" 0 0 5 \
$(gettext 'no')  \"$(gettext 'Standard (humongous) initrd')\" on \
$(gettext 'yes') \"$(gettext 'Small initrd')\" off")
	next_wizard_step
}

### choose whether we want to split the initrd
choose_compression() {
	local msg
	
	msg="$(gettext 'Choose the compression you want to use.')
$(gettext 'In order of size: xz, gzip, lzo lz4.')
$(gettext 'In order of processing time: lz4, lzo, gzip, xz.')"
	MKSQUASHFS_OPTIONS=$(eval "Xdialog $(std_wizard_flags 1) --inputbox \"$msg\" 0 0 \"$MKSQUASHFS_OPTIONS\"")
	MKSQUASHFS_OPTIONS=${MKSQUASHFS_OPTIONS:-$DEFAULT_MKSQUASHFS_OPTIONS}	
	next_wizard_step
}


### confirm settings
confirmation() {
	local msg BACKTITLE savelocation_freespace
	
	savelocation_freespace=$(df -h $savelocation | awk 'NR==2 {print $4}')
	msg=$(cat <<EOF
$(eval_gettext 'Where the CD image will be saved: $savelocation')
$(eval_gettext 'Free space available in $savelocation: $savelocation_freespace')
$(eval_gettext 'Required disk space for output: $tmp_size')
$(eval_gettext 'Mksquashfs compression option: $MKSQUASHFS_OPTIONS')

$(gettext 'Kernel location: $kernellocation')
$([ "$efibootlocation" ] && echo "$(gettext 'EFI bootimage location: $efibootlocation')\n")
$(gettext 'Source layers:')
$(map_layers_to_locations)

$([ $small_initrd = "yes" ] && echo "$(gettext 'Using small initrd.')")

$(gettext 'Make sure you get everything correct.')
$(gettext 'If you click "Next", the remaster will start immediately.')
$(gettext 'If some of the above are wrong, click "Previous" now and correct them.')
EOF
)
	BACKTITLE="$(gettext 'Confirm Selected Settings')"
	eval "Xdialog $(std_wizard_flags 1) --left --yesno \"$msg\" 0 0"	
	next_wizard_step
}

customise() {
	local p
	# clean-up /var - don't want these in the remaster (leave the pkglist, though)
	find $tmp_aufs/var \( -path '*/var/log/packages/*' -prune \) -o \( -type f -o -type s \) -delete

	# remove AUFS_ROOT - don't want it in the remaster
	rm -rf $tmp_aufs/$AUFS_ROOT
	
	# open folder and ask use to add/remove/edit files
	rox -d $tmp_aufs
	Xdialog --title "$APPTITLE" --backtitle "$(gettext 'Add/remove/edit files for remastered copy')" \
--msgbox "$(gettext 'A folder window has been opened. 
This folder contains all the files that will be part of the remaster.
You can add/remove files into/from this folder, or edit them as necessary.
Anything you do here will not affect or change the running system.')
$(gettext 'When done, click OK to continue; Or just click OK now if you do not want to change anything.')" 0 0
	rox -D $tmp_aufs
	go_next
}

### go and do it - the heavylifting here
create_remaster() {
	local tmp_initrd tmp_isoroot XPID EDITOR_PID

	# tell user to be patient
	Xdialog --title "$APPTITLE" --no-buttons --infobox "$(eval_gettext 'Creating $OUTPUT on $savelocation, please wait  ...')" 3 60 1000000 &	
	XPID=$!	
	
	# create temporary directories
	tmp_initrd=$(mktemp -dp "$savelocation" tmp-initrd-XXXXXX)
	tmp_isoroot=$(mktemp -dp "$savelocation" tmp-isoroot-XXXXXX)
	chmod 0755 $tmp_isoroot $tmp_initrd
		
	# re-create the initrd from our baseline
	cp -a $BASELINE_MOUNT/* "$tmp_initrd"
	
	# create the missing /init symlink
	cd "$tmp_initrd"
	ln -s sbin/system-init init

	# re-create kernel-modules.sfs
	if [ $small_initrd = yes ]; then
		# copy contents of kernel-modules to base sfs
		cp -a $MODULE_SFS_MOUNT/* "$tmp_aufs"
		depmod -a -b "$tmp_aufs"
		
		# generate minimum set of modules - this is the same procedure as in fatdog-split-initrd
		tmp_modules=$(mktemp -dp /tmp remaster-modules.XXXXXXXX)
		chmod 0755 $tmp_modules
		cp -a $MODULE_SFS_MOUNT/* $tmp_modules
		get_modules_dep "$BASE_MODULES" $(echo $tmp_modules/lib/modules/*/modules.dep)
		
		# convert this to find params
		FIND_PARAM=""
		for p in $BASE_MODULES; do
			FIND_PARAM="$FIND_PARAM -a ! -name $p.ko"
		done
		FIND_PARAM=${FIND_PARAM#*-a}

		# remove all files that we don't need
		find $tmp_modules/* \( -type f $FIND_PARAM -o -type l \) -delete
		find $tmp_modules/* -type d -empty -delete

		# depmod then we're finished
		depmod -a -b $tmp_modules
		mksquashfs $tmp_modules kernel-modules.sfs $MKSQUASHFS_OPTIONS
		rm -rf $tmp_modules
		
		# change default base_sfs from "initrd" to "local" so that it will search basesfs on local drives
		# while we're at it, increase the default delay to 5 seconds too
		sed -i -e '
s/BASE_SFS_DEFAULT=initrd/BASE_SFS_DEFAULT=local/
s/DEFAULT_DEVICE_DELAY=0/DEFAULT_DEVICE_DELAY=5/
' sbin/system-init 
		
	else
		# re-create kernel-modules.sfs from the loop device
		dd if=$(grep -m 1 $MODULE_SFS_MOUNT /proc/mounts | awk '{print $1}') of=kernel-modules.sfs bs=1M	
	fi
	
	# create the base sfs - this is the main remaster process, really.
	mksquashfs "$tmp_aufs" $BASESFS -noappend $MKSQUASHFS_OPTIONS
	[ $small_initrd = yes ] && mv $BASESFS "$tmp_isoroot" # if small initrd, move it outside
	
	
	# build initrd, put it to iso-root
	find . | cpio -o -H newc > "$tmp_isoroot/initrd"
	
	# populate iso-root with other stuff - isolinux.bin, vmlinuz and isolinux.cfg
	cd "$tmp_isoroot"
	cp "$kernellocation" vmlinuz
	for p in $ISOLINUX_BINS; do
		cp $ISOLINUX_PATH/$p $p
	done
	cp $LOGO_FILE fatdog.png
	cat > isolinux.cfg << EOF
default fatdog
prompt 1
timeout 100
	
ui vesamenu.c32
menu resolution 800 600
menu title Fatdog64 Remastered
menu background fatdog.png
menu tabmsg $(gettext 'Press Tab to edit entry, Esc for boot prompt')
menu color border 37;40  #80ffffff #00000000 std
menu color sel 7;37;40 #80ffffff #20ff8000 all
menu margin 1
menu rows 20
menu tabmsgrow 26
menu cmdlinerow -2
menu passwordrow 19
menu timeoutrow 28
menu helpmsgrow 30

label fatdog
linux vmlinuz
initrd initrd
append rootfstype=ramfs
menu label Fatdog64 Remastered
text help
Start Fatdog64.
endtext

EOF
	# efi boot support
	if [ "$efibootlocation" ]; then
		cp "$efibootlocation" efiboot.img
		cat > grub.cfg << EOF
insmod png
background_image /fatdog.png
set timeout=10
menuentry "Fatdog64 Remastered" {
	linux /vmlinuz rootfstype=ramfs
	initrd /initrd
}		
EOF
	fi
	
	# customise isolinux.cfg & grub.cfg
	geany -i grub.cfg isolinux.cfg &
	EDITOR_PID=$!; sleep 1
	Xdialog --title "$APPTITLE" --backtitle "$(gettext 'Customise isolinux.cfg and grub.cfg')" \
--msgbox "$(gettext 'A text editor has been opened with a copy of the default isolinux.cfg and grub.cfg in it. 
This file contains the default instructions for the CD/DVD bootloader.
You can modify this file to change the default boot parameters.
Once you are finished, save the file (use "Save", not "Save As"), then click OK to continue. 
Or just click OK now if you do not want to change anything.')

$(gettext 'Note: isolinux.cfg is used for normal boot, grub.cfg is used for EFI boot.')" 0 0 
	kill -CONT $EDITOR_PID; kill -HUP $EDITOR_PID
	
	# build the iso
	cd "$savelocation"
	rm $OUTPUT	# mkisofs here must be the same as in build-iso.sh and rc.shutdown
	
	if ! [ "$efibootlocation" ]; then
		# standard iso
		mkisofs -iso-level 4 -D -R -o $OUTPUT -b isolinux.bin -no-emul-boot -boot-load-size 4 -boot-info-table "$tmp_isoroot"
		isohybrid -o 64 $OUTPUT
	else
		# EFI iso
		mkisofs -iso-level 4 -D -R -o $OUTPUT -b isolinux.bin -no-emul-boot -boot-load-size 4 -boot-info-table \
		-eltorito-alt-boot -eltorito-platform efi -b efiboot.img -no-emul-boot "$tmp_isoroot"		
		isohybrid -u $OUTPUT
	fi
	
	# done, cleanup
	rm -rf "$tmp_initrd" "$tmp_isoroot"
	kill $XPID
	
	Xdialog --title "$APPTITLE" --infobox "$(eval_gettext 'Done! $OUTPUT is now be found at $savelocation.') \
$(gettext 'I will open the folder for you.')
$(gettext 'You can now use Pburn (under Multimedia menu) to burn it to CD/DVD.')" 0 0 10000
	su $USER -c "rox -d \"$savelocation\""
	go_next
}


################## main ###################
[ "$NOT_USING_AUFS" ] && exit 1
[ $(id -u) -ne 0 ] && exec gtksu "$APPTITLE" "$0"
while true; do 
	case $step in 
		1) check_prerequisites ;;	
		2) introduction ;;
		3) get_vmlinuz ;;
		4) get_efiboot ;;
		5) choose_layers ;;
		6) choose_save_location ;;
		7) choose_split ;;
		8) choose_compression ;;
		9) confirmation ;;
		10) customise ;;
		11) create_remaster ;;
		12|exit) cleanup; break ;;
		cancel) confirm_cancel ;;
	esac
done

