#!/bin/bash
# mkinitcpio - modular tool for building an init ramfs cpio image
#
# IMPORTANT: We need to keep a common base syntax here because
# some of these hooks/scripts need to run under busybox's ash -
# therefore, the following constraints should be enforced:
#   variables should be quoted and bracketed "${SOMEVAR}"
#   inline execution should be done with $() instead of backticks
#   use -z "${var}" to test for nulls/empty strings
#   in case of embedded spaces, quote all path names and string comparisons
#

shopt -s extglob

# Settings
KERNELVERSION=$(uname -r)
CARCH=$(uname -m)
FUNCTIONS=functions
CONFIG=mkinitcpio.conf
HOOKDIR=hooks
INSTDIR=install
PRESETDIR=mkinitcpio.d
COMPRESSION=gzip

declare TMPDIR BASEDIR MODULE_FILE GENIMG PRESET COMPRESSION_OPTIONS BUILDROOT LD_SO
declare NC= BOLD= BLUE= GREEN= RED= YELLOW=
declare -i QUIET=1 SHOW_AUTOMODS=0 SAVELIST=0 COLOR=1
declare -a SKIPHOOKS ADDED_MODULES

# Add /{,usr}/sbin to path
# works around undetected problems like in #8448
PATH=$PATH:/sbin:/usr/sbin
# Sanitize environment further
# GREP_OPTIONS="--color=always" will break everything
unset GREP_OPTIONS

APPNAME=${0##*/}

usage() {
    cat <<EOF
usage: $APPNAME [options]

  Options:
   -b BASEDIR       Use BASEDIR. default: /
   -c CONFIG        Use CONFIG file. default: /etc/mkinitcpio.conf
   -g IMAGE         Generate a cpio image as IMAGE. default: no
   -H HOOKNAME      Output help for hook 'HOOKNAME'.
   -h               Display this message.
   -k KERNELVERSION Use KERNELVERSION. default: $(uname -r)
   -L               List all available hooks.
   -M               Display modules found via autodetection.
   -n               Disable colorized output messages.
   -p PRESET        Build specified preset from /etc/mkinitcpio.d
   -S SKIPHOOKS     Skip SKIPHOOKS (comma-separated) when building the image.
   -s               Save build directory. default: no
   -t TMPDIR        Use TMPDIR as the temporary build directory.
   -v               Verbose output. Default: no.
   -z COMPRESS      Use COMPRESS on resulting image

EOF
    cleanup
    exit 1
}

cleanup ()
{
    if [[ $TMPDIR ]]; then
        if (( $SAVELIST )); then
            msg "build directory saved in %s" "$TMPDIR"
        else
            rm -rf "$TMPDIR"
        fi
    fi
}

sighandler() {
    cleanup
    exit 1
}

get_kernver() {
    local kernel=$1

    if [[ "${kernel:0:1}" != / ]]; then
        echo $kernel
        return 0
    fi

    [[ -r "$kernel" ]] || return 1

    read _ kernver < <(file -b "$BASEDIR$kernel" | grep -o 'version [^ ]\+')
    if [[ "$kernver" && -e "$BASEDIR/lib/modules/$kernver" ]]; then
        echo "$kernver"
        return 0
    fi

    return 1
}

. "$FUNCTIONS"

trap sighandler TERM INT

while getopts ':c:k:sb:g:p:m:nvH:LMhS:t:z:' arg; do
    case "${arg}" in
        c) CONFIG="${OPTARG}" ;;
        k) optkver=$OPTARG ;;
        s) SAVELIST=1; ;;
        b) BASEDIR="${OPTARG}" ;;
        g) GENIMG="${OPTARG}" ;;
        h) usage ;;
        p) PRESET="${OPTARG}" ;;
        n) COLOR=0 ;;
        v) QUIET=0 ;;
        S) OLDIFS=${IFS}
           IFS=,
           SKIPHOOKS=(${OPTARG})
           IFS=${OLDIFS}
           unset OLDIFS
           ;;
        H) if [[ ! -r "${INSTDIR}/${OPTARG}" ]]; then
               error "No hook ${OPTARG}"
               exit 1
           fi
           . "${INSTDIR}/${OPTARG}"
           if [[ $(type -t help) != function ]]; then
               error "No help for hook ${OPTARG}"
               exit 1
           fi
           echo "Help for hook '${OPTARG}':"
           help
           exit 0 ;;
        L) msg "Available hooks"
           cd "$INSTDIR" >/dev/null
           printf '   %s\n' * | column -c$(tput cols)
           exit 0 ;;
        M) SHOW_AUTOMODS=1 ;;
        t) TMPDIR=$OPTARG ;;
        z) optcompress=$OPTARG ;;
        :) error "option requires an argument -- '$OPTARG'" >&2
           exit 1 ;;
       \?) error "invalid option -- '$OPTARG'" >&2
           exit 1 ;;
    esac
done
shift $((OPTIND - 1))

if [[ -t 2 ]] && (( COLOR )); then
    # prefer terminal safe colored and bold text when tput is supported
    if tput setaf 0 &>/dev/null; then
        NC="$(tput sgr0)"
        BOLD="$(tput bold)"
        BLUE="$BOLD$(tput setaf 4)"
        GREEN="$BOLD$(tput setaf 2)"
        RED="$BOLD$(tput setaf 1)"
        YELLOW="$BOLD$(tput setaf 3)"
    else
        NC="\e[1;0m"
        BOLD="\e[1;1m"
        BLUE="$BOLD\e[1;34m"
        GREEN="$BOLD\e[1;32m"
        RED="$BOLD\e[1;31m"
        YELLOW="$BOLD\e[1;33m"
    fi
fi
readonly NC BOLD BLUE GREEN RED YELLOW

if [[ $optkver ]]; then
    if ! KERNELVERSION=$(get_kernver "$optkver"); then
         die "'$optkver' is an invalid kernel specifier"
    fi
fi

if [[ $TMPDIR ]]; then
    if [[ ! -d $TMPDIR ]]; then
        error "'$TMPDIR' does not exist or is not a directory"
        unset TMPDIR
        cleanup
        exit 1
    fi
fi
TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/mkinitcpio.XXXXXX")
declare BUILDROOT=$TMPDIR/root

# use preset $PRESET
if [[ $PRESET ]]; then
    # allow absolute path to preset file, else resolve it
    if [[ "${PRESET:0:1}" != '/' ]]; then
        printf -v PRESET '%s/%s.preset' "$PRESETDIR" "$PRESET"
    fi
    if [[ -f "$PRESET" ]]; then
        # Use -b, -m and -v options specified earlier
        declare -a preset_mkopts preset_cmd
        [[ $BASEDIR ]] && preset_mkopts+=(-b "$BASEDIR")
        (( QUIET )) || preset_mkopts+=(-v)
        # Build all images
        . "$PRESET"
        for p in "${PRESETS[@]}"; do
            msg "Building image from preset: '$p'"
            preset_cmd=("${preset_mkopts[@]}")

            preset_kver=${p}_kver
            if [[ ${!preset_kver:-$ALL_kver} ]]; then
                preset_cmd+=(-k "${!preset_kver:-$ALL_kver}")
            else
                warning "No kernel version specified. Skipping image '%s'" "$p"
                continue
            fi

            preset_config=${p}_config
            if [[ ${!preset_config:-$ALL_config} ]]; then
              preset_cmd+=(-c "$BASEDIR${!preset_config:-$ALL_config}")
            else
                warning "No configuration file specified. Skipping image '%s'" "$p"
                continue
            fi

            preset_image=${p}_image
            if [[ ${!preset_image} ]]; then
                preset_cmd+=(-g "$BASEDIR${!preset_image}")
            else
                warning "No image file specified. Skipping image '%s'" "$p"
                continue
            fi

            preset_options=${p}_options
            if [[ ${!preset_options} ]]; then
                preset_cmd+=(${!preset_options}) # intentional word splitting
            fi

            msg2 "${preset_cmd[*]}"
            "$0" "${preset_cmd[@]}"
        done
        cleanup
        exit 0
    else
        die "Preset $PRESET does not exist. Exiting."
    fi
fi

if [[ $BASEDIR ]]; then
    # resolve the path. it might be a relative path and/or contain symlinks
    abspath=$(readlink -e "$BASEDIR")
    if [[ -z $abspath ]]; then
        die "base directory '$BASEDIR' does not exist or is not a directory"
    fi
    BASEDIR=$abspath
    unset abspath
fi

if [[ $GENIMG ]]; then
    IMGPATH=$(readlink -f "$GENIMG")
    if [[ -z $IMGPATH || ! -w ${IMGPATH%/*} ]]; then
      die "error: unable to write to path: '$GENIMG'"
    fi
fi

if [[ ! -f "$CONFIG" ]]; then
    die "config file '$CONFIG' cannot be found, aborting..."
fi
. "$CONFIG"

MODULEDIR=$BASEDIR/lib/modules/$KERNELVERSION
if [[ ! -d $MODULEDIR ]]; then
    die "'$MODULEDIR' is not a valid kernel module directory"
fi

if (( SHOW_AUTOMODS )); then
    msg "Modules autodetected"
    . "${INSTDIR}/autodetect"
    build
    cat "${MODULE_FILE}"
    cleanup
    exit 0
fi


if [[ -z $GENIMG ]]; then
    msg "Starting dry run: %s" "$KERNELVERSION"
else
    COMPRESSION=${optcompress:-$COMPRESSION}
    if ! type -P "$COMPRESSION" >/dev/null; then
        die "Unable to locate compression method: %s" "$optcompress"
    fi

    msg "Starting build: %s" "$KERNELVERSION"
fi

# set errtrace and a trap to catch errors in parse_hook
declare -i builderrors=0
set -E
trap '[[ $FUNCNAME = parse_hook ]] && (( ++builderrors ))' ERR

#parse 'global' hook, as defined in ${CONFIG}
parse_hook

# resolve the linker and add it
case $CARCH in
  i686) LD_SO=("$BASEDIR"/lib/ld-linux.so.?*) ;;
  x86_64) LD_SO=("$BASEDIR"/lib/ld-linux-${CARCH//_/-}.so.?*) ;;
  *) die "unknown architecture: $CARCH" ;;
esac

if (( ${#LD_SO[*]} != 1 )); then # uh oh...
  die "failed to resolve the location of /lib/ld.so. Please report this bug."
fi

resolved=$(readlink -e "$LD_SO")
_add_file "${resolved#$BASEDIR}" "$resolved" 755
_add_symlink "${LD_SO#$BASEDIR}" "$resolved"
unset resolved

for hook in ${HOOKS}; do
    in_array "$hook" "${SKIPHOOKS[@]}" && continue
    unset MODULES BINARIES FILES SCRIPT
    build () { error "$hook: no build function..."; }

    # A hook is considered deprecated if it is a symlink within $INSTDIR.
    if [[ -L "$INSTDIR/$hook" ]]; then
        newhook=$(readlink -e "$INSTDIR/$hook")
        if [[ $newhook && "$INSTDIR/${newhook##*/}" -ef "$newhook" ]]; then
            newhook=${newhook##*/}
            warning "Hook '%s' is deprecated. Replace it with '%s' in your config" "$hook" "$newhook"
            hook=$newhook
        fi
    fi
    if [[ -r "${INSTDIR}/${hook}" ]]; then
        . "${INSTDIR}/${hook}"
        msg2 "Parsing hook: [%s]" "$hook"
        if [[ $(type -t install) = 'function' ]]; then
            warning "Hook '%s' uses a deprecated 'install' function. This should be renamed 'build'" "$hook"
            install
            unset install
        else
            build
        fi
        parse_hook
    else
        die "Hook '$hook' can not be found."
    fi
done

# unset errtrace and trap
set +E
trap ERR

if (( ${#ADDED_MODULES[*]} )); then
    msg "Generating module dependencies"
    /sbin/depmod -b "${TMPDIR}/root" "${KERNELVERSION}"
    rm "$BUILDROOT/lib/modules/$KERNELVERSION"/modules.!(dep.bin|alias.bin|symbols.bin)
fi

declare -i status=0
declare -a pipesave
if [[ "${GENIMG}" ]]; then
    msg "Creating $COMPRESSION initcpio image: %s" "$GENIMG"
    [[ "$COMPRESSION" = xz ]] && COMPRESSION_OPTIONS+=" --check=crc32"

    pushd "$BUILDROOT" >/dev/null
    find . -print0 | bsdcpio -0oH newc | $COMPRESSION $COMPRESSION_OPTIONS > "$IMGPATH"
    pipesave=("${PIPESTATUS[@]}") # save immediately
    popd >/dev/null

    if (( pipesave[0] )); then
        errmsg="find reported an error"
    elif (( pipesave[1] )); then
        errmsg="bsdcpio reported an error"
    elif (( pipesave[2] )); then
        errmsg="$COMPRESSION reported an error"
    fi

    if (( builderrors )); then
        warning "errors were encountered during the build. The image may not be complete."
        status=1
    fi

    if [[ $errmsg ]]; then
        error "Image generation FAILED: %s\n" "$errmsg"
        status=1
    else
        msg "Image generation successful"
    fi

else
    msg "Dry run complete, use -g IMAGE to generate a real image"
fi

cleanup

exit ${status}

# vim: set ft=sh ts=4 sw=4 et:
