#!/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
#

declare -r version=%VERSION%

shopt -s extglob

# Settings
FUNCTIONS=functions
CONFIG=mkinitcpio.conf
HOOKDIR=(hooks /usr/lib/initcpio/hooks /lib/initcpio/hooks)
INSTDIR=(install /usr/lib/initcpio/install /lib/initcpio/install)
PRESETDIR=mkinitcpio.d
COMPRESSION=gzip

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

# export a sane PATH
export PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'

# Sanitize environment further
# GREP_OPTIONS="--color=always" will break everything
# CDPATH can affect cd and pushd
unset GREP_OPTIONS CDPATH

usage() {
    cat <<EOF
mkinitcpio $version
usage: ${0##*/} [options]

  Options:
   -A, --add <hooks>            Add specified hooks, comma separated, to image
   -b, --basedir <dir>          Use alternate base directory. (default: /)
   -c, --config <config>        Use alternate config file. (default: /etc/mkinitcpio.conf)
   -g, --generate <path>        Generate cpio image and write to specified path
   -H, --hookhelp <hookname>    Display help for given hook and exit
   -h, --help                   Display this message and exit
   -k, --kernel <kernelver>     Use specified kernel version (default: $(uname -r))
   -L, --listhooks              List all available hooks
   -M, --automods               Display modules found via autodetection
   -n, --nocolor                Disable colorized output messages
   -p, --preset <file>          Build specified preset from /etc/mkinitcpio.d
   -S, --skiphooks <hooks>      Skip specified hooks, comma-separated, during build
   -s, --save                   Save build directory. (default: no)
   -t, --builddir <dir>         Use DIR as the temporary build directory
   -v, --verbose                Verbose output (default: no)
   -z, --compress <program>     Use an alternate compressor on the image

EOF
}

cleanup() {
    if [[ $workdir ]]; then
        # when PRESET is set, we're in the main loop, not a worker process
        if (( SAVELIST )) && [[ -z $PRESET ]]; then
            msg "build directory saved in %s" "$workdir"
        else
            rm -rf "$workdir"
        fi
    fi

    exit ${1:0}
}

get_kernver() {
    local kernver= kernel=$1

    if [[ -z $kernel ]]; then
        uname -r
        return 0
    fi

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

    if [[ ! -e $BASEDIR$kernel ]]; then
        error "Specified kernel image does not exist: \`%s'" "$BASEDIR$kernel"
        return 1
    fi

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

    [[ ${optkver:0:1} == / ]] && optkver=$BASEDIR$optkver
    error "invalid kernel specifier: \`%s'" "$optkver"

    return 1
}

compute_hookset() {
    for h in $HOOKS "${ADDHOOKS[@]}"; do
        in_array "$h" "${SKIPHOOKS[@]}" && continue
        hooks+=("$h")
    done
}

. "$FUNCTIONS"

trap 'cleanup 130' INT
trap 'cleanup 143' TERM

OPT_SHORT='A:b:c:g:H:hk:mnLMp:S:st:vz:'
OPT_LONG=('add:' 'basedir:' 'config:' 'generate:' 'hookhelp:' 'help' 'kernel:' 'listhooks'
          'automods' 'nocolor' 'preset:' 'skiphooks:' 'save' 'builddir:' 'verbose' 'compress:')

if ! parseopts "$OPT_SHORT" "${OPT_LONG[@]}" -- "$@"; then
    exit 1
fi
set -- "${OPTRET[@]}"
unset OPT_SHORT OPT_LONG OPTRET

while :; do
    case $1 in
        -A|--add)
            shift
            IFS=, read -r -a add <<< "$1"
            ADDHOOKS+=("${add[@]}")
            unset add ;;
        -c|--config)
            shift
            CONFIG=$1 ;;
        -k|--kernel)
            shift
            optkver=$1 ;;
        -s|--save)
            SAVELIST=1 ;;
        -b|--basedir)
            shift
            BASEDIR=$1 ;;
        -g|--generate)
            shift
            GENIMG=$1 ;;
        -h|--help)
            usage
            cleanup 0 ;;
        -p|--preset)
            shift
            PRESET=$1 ;;
        -n|--nocolor)
            COLOR=0 ;;
        -v|--verbose)
            QUIET=0 ;;
        -S|--skiphooks)
            shift
            IFS=, read -r -a skip <<< "$1"
            SKIPHOOKS+=("${skip[@]}")
            unset skip ;;
        -H|--hookhelp)
            shift
            if script=$(find_in_dirs "$1" "${INSTDIR[@]}"); then
                . "$script"
                if [[ $(type -t help) != function ]]; then
                    error "No help for hook $1"
                    exit 1
                fi
            else
                error "No hook '$1'"
                exit 1
            fi
            msg "Help for hook '$1':"
            help
            exit 0 ;;
        -L|--listhooks)
            msg "Available hooks"
            for dir in "${INSTDIR[@]}"; do
                ( cd "$dir" &>/dev/null && printf '   %s\n' * )
            done | sort -u | column -c$(tput cols)
            exit 0 ;;
        -M|--automods)
            SHOW_AUTOMODS=1 ;;
        -t|--builddir)
            shift
            TMPDIR=$1 ;;
        -z|--compress)
            shift
            optcompress=$1 ;;
        --)
            shift
            break 2 ;;
    esac
    shift
done

if [[ -t 1 ]] && (( 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

# insist that /proc and /dev be mounted (important for chroots)
# NOTE: avoid using mountpoint for this -- look for the paths that we actually
# use in mkinitcpio. Avoids issues like FS#26344.
[[ -e /proc/self/mountinfo ]] || die "/proc must be mounted!"
[[ -e /dev/fd ]] || die "/dev must be mounted!"

if [[ $BASEDIR ]]; then
    # resolve the path. it might be a relative path and/or contain symlinks
    if ! pushd "$BASEDIR" &>/dev/null; then
        die "base directory does not exist or is not a directory: \`%s'" "$BASEDIR"
    fi
    BASEDIR=$(pwd -P)
    BASEDIR=${BASEDIR%/}
    popd &>/dev/null
fi

KERNELVERSION=$(get_kernver "$optkver") || cleanup 1

if [[ $TMPDIR ]]; then
    if [[ ! -d $TMPDIR ]]; then
        error "Temporary directory does not exist or is not a directory: \`%s'" "$TMPDIR"
        cleanup 1
    fi
    if [[ ! -w $TMPDIR ]]; then
        error "Temporary directory is not writeable: \`%s'" "$TMPDIR"
        cleanup 1
    fi
fi
workdir=$(TMPDIR=$TMPDIR mktemp -d --tmpdir mkinitcpio.XXXXXX)
BUILDROOT=$workdir/root

# explicitly create the buildroot
mkdir -p "$BUILDROOT/usr/lib/modules/$KERNELVERSION/kernel"

# 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 0
    else
        die "Preset not found: \`%s'" "$PRESET"
    fi
fi

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

if [[ ! -f "$CONFIG" ]]; then
    die "Config file does not exist: \`%s'" "$CONFIG"
fi
. "$CONFIG"

# after returning, hooks are populated into the array 'hooks'
# HOOKS should not be referenced from here on
compute_hookset

if (( ${#hooks[*]} == 0 )); then
    die "Invalid config: No hooks found"
fi

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"
    autodetect=$(find_in_dirs 'autodetect' "${INSTDIR[@]}")
    . "$autodetect"
    build
    cat "$MODULE_FILE"
    cleanup 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" "$COMPRESSION"
    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

# save vars from $CONFIG; they will be parsed last
for var in MODULES BINARIES FILES; do
    declare "cfg_$var=${!var}"
done

for hook in "${hooks[@]}"; do
    unset MODULES BINARIES FILES SCRIPT
    build() { error "$hook: no build function..."; return 1; }

    # find script in install dirs
    if ! script=$(find_in_dirs "$hook" "${INSTDIR[@]}"); then
        error "Hook '$hook' cannot be found."
        (( ++builderrors ))
        continue
    fi

    # check for deprecation
    if [[ -L $script ]]; then
        if ! realscript=$(readlink -e "$script"); then
            error "$script is a broken symlink to $(realpath "$script")"
            (( ++builderrors ))
            continue
        fi
        warning "Hook '%s' is deprecated. Replace it with '%s' in your config" "$script" "$realscript"
        script=$realscript
    fi

    # source
    if ! . "$script"; then
        error 'Failed to read %s' "$script"
        (( ++builderrors ))
        continue
    fi

    # run
    msg2 "Parsing hook: [%s]" "${script##*/}"
    build
    parse_hook
done

# restore $CONFIG vars add to image
for var in cfg_{MODULES,BINARIES,FILES}; do
    declare "${var#cfg_}=${!var}"
done
parse_hook

# reset the trap to catch all errors
trap '(( ++builderrors ))' ERR

if (( ${#ADDED_MODULES[*]} )); then
    printf '%s\0' "${MODPATHS[@]}" | sort -zu |
        xargs -0 cp -t "$BUILDROOT/usr/lib/modules/$KERNELVERSION/kernel"

    # unzip modules prior to recompression
    gzip -dr "$BUILDROOT/usr/lib/modules/$KERNELVERSION/kernel"

    msg "Generating module dependencies"
    install -m644 -t "$BUILDROOT/usr/lib/modules/$KERNELVERSION" \
        "$BASEDIR/lib/modules/$KERNELVERSION"/modules.{builtin,order}
    depmod -b "$BUILDROOT" "$KERNELVERSION"

    # remove all non-binary module.* files (except devname for on-demand module loading)
    rm "$BUILDROOT/usr/lib/modules/$KERNELVERSION"/modules.!(*.bin|devname)

else
    warning "No modules were added to the image. This is probably not what you want."
fi

# unset errtrace and trap
set +E
trap ERR

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

    # write version stamp
    printf '%s' "$version" > "$BUILDROOT/VERSION"

    pushd "$BUILDROOT" >/dev/null
    find . -print0 |
        bsdcpio $( (( QUIET )) && echo '--quiet' ) -R 0:0 -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" "$errmsg"
        status=1
    else
        msg "Image generation successful"
    fi

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

cleanup $status

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