#!/bin/bash
#
# redboot-install
# - a tool to create a bootable SD card on a babbage board
#
# Copyright (c) 2009 Canonical
# Authors: Oliver Grawert <ogra@canonical.com>
#          Loïc Minier <loic.minier@canonical.com>
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License as
#  published by the Free Software Foundation; either version 2 of the
#  License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
#
# Required packages: redboot-imx51-babbage, redboot-tools, linux-imx51
#
# TODO
# - use safer -m flag of parted (needs a newer parted)

DEV=""

DIST="jaunty"
FLAVOUR="imx51"
BOARD="babbage"
HWITER="-TO2"

trap cleanup 0 1 2 3 6

cleanup()
{
    if [ -d "${BUILDDIR}" ]; then
		echo "cleaning up ..."
		rm -rf $BUILDDIR
		echo "done ..."
	fi
	exit 0
}

usage()
{
echo '
usage: redboot-install -d <device> [option]

required options:
-d --device <device>
	Target device to write redboot setup to

additional options:
-h --help
	This help
-k --kernel
	Location of the vmlinuz/zImage file with full path
	Defaults to: /boot/vmlinuz-$(uname -r)
-i --initrd
	Location of the initrd.gz file with full path
	Defaults to: /boot/initrd.img-$(uname -r)
-c --cmdline
	Kernel commandline to use for booting
	Defaults to: root=UUID=<uuid of current /> ro quiet
-n --nodev
	Allow argument to -d to not be a blockdevice
	(i.e. for use with an image file)
'
exit 0
}

checkparm()
{
    if [ "$(echo $1|grep ^'\-')" ] || [ -z "$1" ];then
        echo "E: Need an argument"
        usage
    fi
}

ALLOW_IMAGE=""

while [ ! -z "$1" ]; do
    case $1 in
        -h|--help)
            usage
			;;
		-d|--device)
			checkparm $2
			DEV="$2"
            ;;
		-k|--kernel)
			checkparm $2
			KERNEL_FIS_DATA="$2"
			;;
		-i|--initrd)
			checkparm $2
			INITRD_FIS_DATA="$2"
			;;
		-c|--cmdline)
			checkparm $2
			CMDLINE="$2"
			;;
		-n|--nodev)
			ALLOW_IMAGE="1"
    esac
    shift
done

if [ ! -n "${DEV}" ];then
    usage
fi

if [ ! -b "${DEV}" ] && [ -z "${ALLOW_IMAGE}" ];then
    echo "E: no such device: $DEV"
    exit 0
fi

if [ ! -w "${DEV}" ];then
    echo "E: cannot write to: $DEV"
    exit 0
fi


file_length() {
    stat -c %s "$1"
}

BUILDDIR=$(mktemp -d)
CMDLINE="${CMDLINE:-ro quiet splash}"
KERNEL_FIS_DATA="${KERNEL_FIS_DATA:-/boot/vmlinuz-$(uname -r)}"
INITRD_FIS_DATA="${INITRD_FIS_DATA:-/boot/initrd.img-$(uname -r)}"

# pick an arbitrary large enough size multiple of 512 (sector size) and of
# 0x20000 (flash block size)
FIS_SIZE="$((16 * 1024 * 1024))"

hex2dec() {
    printf "%d\n" "$1"
}

# create partition table
echo "initializing disk label (MBR and partition table)..."
parted -s "$DEV" mklabel msdos

echo "creating FIS partition..."
# 512 bytes is the smallest offset where the partition can start; note that the
# FIS starts on the first sector (LBA address 0 or offset 0); also note that
# parted END address is inclusive, so we substract 1 from FIS_SIZE
parted -s "$DEV" mkpart primary fat32 "512B" "$(($FIS_SIZE - 1))B"

# hackish way to set partition type to "Non-FS data" (0xda); neither parted
# not fdisk work well in all cases here; fdisk will complain about lack of
# number of cylinders, and parted doesn't take arbitrary ids
# partition table starts at 0x01BE, partition type is at +0x04
PART1_ID_OFFSET="$(hex2dec 0x1c2)"
printf '\xda' | dd conv=notrunc bs="$PART1_ID_OFFSET" of="$DEV" seek=1 2>/dev/null

# outputs actual partition start offset, end offset, and length, suffixed with
# B
get_part_data() {
    local n="$1"

    LANG=C parted -s "$DEV" unit B print | awk "/^ $n / { print \$2 \" \" \$3 \" \" \$4 }"

    # safer version using parted -m; needs newer parted
    #LANG=C parted -m -s "$DEV" unit B print | grep "^$n:" | cut -d: -f 2,3,4 --output-delimiter=" "
}

PART1_END_B="`(set -- $(get_part_data 1); echo "$2")`"
if [ "$((${PART1_END_B%B} + 1))" -lt "$FIS_SIZE" ]; then
    echo "FIS partition ends at $PART1_END_B and doesn't leave enough room for FIS ${FIS_SIZE}B"
fi

REDBOOT_PKG="redboot-$FLAVOUR-$BOARD"
REDBOOT_DATA="/usr/lib/redboot/$FLAVOUR-${BOARD}${HWITER}_redboot.bin"
CONFIG_DATA="/usr/lib/redboot/$FLAVOUR-${BOARD}_fconfig.bin"

# the FIS config depends of the target board; offsets are converted to decimal
# for dd and "test"
FIS_DIR_OFFSET="$(hex2dec 0x40000)"
FIS_DIR_LENGTH="$(hex2dec 0x1F000)"
FIS_DIR_ADDR="$(hex2dec 0x40000)"
# where to actually write RedBoot
REDBOOT_OFFSET="$(hex2dec 0x400)"
# the address to write in the FIS entry; we could theoritically write 0x400
# here and decrease the entry length by the same amount, but eCos/RedBoot don't
# like writing at addresses not aligned to 0x20000, so avoid that; it's not
# clear whether the ROM loads 0x0 - 0x400 in memory, or whether the romupdate
# command is clever enough to avoid this issue on write, but it's not a problem
# because the romupdate command is a no-op when booting from SD
REDBOOT_FIS_OFFSET="$(hex2dec 0x0)"
REDBOOT_FIS_LENGTH="$(hex2dec 0x40000)"
CONFIG_FIS_OFFSET="$(hex2dec 0x5F000)"
CONFIG_FIS_LENGTH="$(hex2dec 0x1000)"
CONFIG_FIS_ADDR="$(hex2dec 0x5F000)"
KERNEL_FIS_OFFSET="$(hex2dec 0x60000)"
KERNEL_FIS_LENGTH="$(hex2dec 0x500000)"
KERNEL_FIS_ENTRY="$(hex2dec 0x100000)"
KERNEL_FIS_ADDR="$(hex2dec 0x100000)"
INITRD_FIS_OFFSET="$(hex2dec 0x560000)"
INITRD_FIS_LENGTH="$(hex2dec 0x940000)"
INITRD_FIS_ENTRY="$(hex2dec 0xFFFFFFFF)"
INITRD_FIS_ADDR="$(hex2dec 0x1000000)"

# wrapper to call the FIS command-line tool
fis_do() {
    fis -d "$DEV" -o "$FIS_DIR_OFFSET" -s "$FIS_DIR_LENGTH" "$@"
}

# helper to write a file's data to the FIS at given offset; also checks the
# file is smaller than length before writing
# NB: this actually uses $offset memory, so don't use too large offsets
fis_write() {
    local file="$1"
    local offset="$2"
    local max_length="$3"

    if [ "$(file_length "$1")" -gt "$max_length" ]; then
        echo "File $file is larger than maximum allowed size of $max_length"
    fi

    dd conv=notrunc bs="$offset" if="$file" of="$DEV" seek=1 2>/dev/null
}

echo "initializing fis directory..."
fis_do init

echo "  'RedBoot'"
fis_do create "RedBoot" \
    -f "$REDBOOT_FIS_OFFSET" \
    -l "$REDBOOT_FIS_LENGTH" \
    -c "$REDBOOT_DATA"
fis_write "$REDBOOT_DATA" "$REDBOOT_OFFSET" "$REDBOOT_FIS_LENGTH"

echo "  'FIS directory'"
fis_do create "FIS directory" \
    -f "$FIS_DIR_OFFSET" \
    -l "$FIS_DIR_LENGTH" \
    -r "$FIS_DIR_ADDR"

echo "writing bootloader configuration..."
# modified bootloader config
# TODO use mktemp
CONFIG_DATA_MODIFIED="fconfig.bin"
cp "$CONFIG_DATA" "$CONFIG_DATA_MODIFIED"
# set a config var to a value
fconfig_set() {
    fconfig -s -w -d "$CONFIG_DATA_MODIFIED" -n "$1" -x "$2"
}
# launch boot script on boot
fconfig_set boot_script TRUE
# after 3 seconds
fconfig_set boot_script_timeout 3
# actual boot script
SCRIPT="fis load initrd\\"
SCRIPT="${SCRIPT}fis load kernel\\"
SCRIPT="${SCRIPT}exec -r $INITRD_FIS_ADDR -s $INITRD_FIS_LENGTH -c \"$CMDLINE\"\\"
fconfig_set boot_script_data "$SCRIPT"
# disable DHCP on boot
fconfig_set bootp FALSE

echo "  'RedBoot config'"
fis_do create "RedBoot config" \
    -f "$CONFIG_FIS_OFFSET" \
    -l "$CONFIG_FIS_LENGTH" \
    -r "$CONFIG_FIS_ADDR" \
    -c "$CONFIG_DATA_MODIFIED"
fis_write "$CONFIG_DATA_MODIFIED" "$CONFIG_FIS_OFFSET" "$CONFIG_FIS_LENGTH"

# purge extracted RedBoot and RedBoot config data
rm -rf usr

echo "  'kernel'"
fis_do create "kernel" \
    -f "$KERNEL_FIS_OFFSET" \
    -l "$KERNEL_FIS_LENGTH" \
    -e "$KERNEL_FIS_ENTRY" \
    -r "$KERNEL_FIS_ADDR" \
    -c "$KERNEL_FIS_DATA"
fis_write "$KERNEL_FIS_DATA" "$KERNEL_FIS_OFFSET" "$KERNEL_FIS_LENGTH"

# pad initrd
INITRD_FIS_DATA_LENGTH="$(file_length "$INITRD_FIS_DATA")"
PADDED_INITRD_FIS_DATA="$INITRD_FIS_DATA.padded"
if [ "$INITRD_FIS_DATA_LENGTH" -gt "$INITRD_FIS_LENGTH" ]; then
    echo "Initrd $INITRD_FIS_DATA too big for FIS initrd partition length ($INITRD_FIS_LENGTH)"
fi
PAD="$(expr "$INITRD_FIS_LENGTH" - "$INITRD_FIS_DATA_LENGTH")"
(
    cat "$INITRD_FIS_DATA"
    # pad with zeroes; this uses $PAD mem, not very elegant
    dd if=/dev/zero bs="$PAD" count=1 2>/dev/null
) | dd of="$PADDED_INITRD_FIS_DATA" bs=4k 2>/dev/null

echo "  'initrd'"
fis_do create "initrd" \
    -f "$INITRD_FIS_OFFSET" \
    -l "$INITRD_FIS_LENGTH" \
    -e "$INITRD_FIS_ENTRY" \
    -r "$INITRD_FIS_ADDR" \
    -c "$PADDED_INITRD_FIS_DATA"
fis_write "$PADDED_INITRD_FIS_DATA" "$INITRD_FIS_OFFSET" "$INITRD_FIS_LENGTH"
rm -f "$PADDED_INITRD_FIS_DATA"
