#!/bin/bash

parseopts() {
    local opt= optarg= i= shortopts=$1
    local -a longopts=() unused_argv=()

    shift
    while [[ $1 && $1 != '--' ]]; do
        longopts+=("$1")
        shift
    done
    shift

    longoptmatch() {
        local o longmatch=()
        for o in "${longopts[@]}"; do
            if [[ ${o%:} = "$1" ]]; then
                longmatch=("$o")
                break
            fi
            [[ ${o%:} = "$1"* ]] && longmatch+=("$o")
        done

        case ${#longmatch[*]} in
            1)
                # success, override with opt and return arg req (0 == none, 1 == required)
                opt=${longmatch%:}
                if [[ $longmatch = *: ]]; then
                    return 1
                else
                    return 0
                fi ;;
            0)
                # fail, no match found
                return 255 ;;
            *)
                # fail, ambiguous match
                printf "%s: option '%s' is ambiguous; possibilities:%s\n" "${0##*/}" \
                    "--$1" "$(printf " '%s'" "${longmatch[@]%:}")"
                return 254 ;;
        esac
    }

    while (( $# )); do
        case $1 in
            --) # explicit end of options
                shift
                break
                ;;
            -[!-]*) # short option
                for (( i = 1; i < ${#1}; i++ )); do
                    opt=${1:i:1}

                    # option doesn't exist
                    if [[ $shortopts != *$opt* ]]; then
                        printf "%s: invalid option -- '%s'\n" "${0##*/}" "$opt"
                        OPTRET=(--)
                        return 1
                    fi

                    OPTRET+=("-$opt")
                    # option requires optarg
                    if [[ $shortopts = *$opt:* ]]; then
                        # if we're not at the end of the option chunk, the rest is the optarg
                        if (( i < ${#1} - 1 )); then
                            OPTRET+=("${1:i+1}")
                            break
                        # if we're at the end, grab the the next positional, if it exists
                        elif (( i == ${#1} - 1 )) && [[ $2 ]]; then
                            OPTRET+=("$2")
                            shift
                            break
                        # parse failure
                        else
                            printf "%s: option '%s' requires an argument\n" "${0##*/}" "-$opt"
                            OPTRET=(--)
                            return 1
                        fi
                    fi
                done
                ;;
            --?*=*|--?*) # long option
                IFS='=' read -r opt optarg <<< "${1#--}"
                longoptmatch "$opt"
                case $? in
                    0)
                        if [[ $optarg ]]; then
                            printf "%s: option '--%s' doesn't allow an argument\n" "${0##*/}" "$opt"
                            OPTRET=(--)
                            return 1
                        else
                            OPTRET+=("--$opt")
                        fi
                        ;;
                    1)
                        # --longopt=optarg
                        if [[ $optarg ]]; then
                            OPTRET+=("--$opt" "$optarg")
                        # --longopt optarg
                        elif [[ $2 ]]; then
                            OPTRET+=("--$opt" "$2" )
                            shift
                        else
                            printf "%s: option '--%s' requires an argument\n" "${0##*/}" "$opt"
                            OPTRET=(--)
                            return 1
                        fi
                        ;;
                    254)
                        # ambiguous option -- error was reported for us by longoptmatch()
                        OPTRET=(--)
                        return 1
                        ;;
                    255)
                        # parse failure
                        printf "%s: unrecognized option '%s'\n" "${0##*/}" "--$opt"
                        OPTRET=(--)
                        return 1
                        ;;
                esac
                ;;
            *) # non-option arg encountered, add it as a parameter
                unused_argv+=("$1")
                ;;
        esac
        shift
    done

    # add end-of-opt terminator and any leftover positional parameters
    OPTRET+=('--' "${unused_argv[@]}" "$@")
    unset longoptmatch

    return 0
}

kver() {
    # this is intentionally very loose. only ensure that we're
    # dealing with some sort of string that starts with something
    # resembling dotted decimal notation. remember that there's no
    # requirement for CONFIG_LOCALVERSION to be set.
    local kver re='^[[:digit:]]+(\.[[:digit:]]+)+'

    # scrape the version out of the kernel image. locate the offset
    # to the version string by reading 2 bytes out of image at at
    # address 0x20E. this leads us to a string of, at most, 128 bytes.
    # read the first word from this string as the kernel version.
    local offset=$(hexdump -s 526 -n 2 -e '"%0d"' "$1")
    [[ $offset = +([0-9]) ]] || return 1

    read kver _ < \
        <(dd if="$1" bs=1 count=127 skip=$(( offset + 0x200 )) 2>/dev/null)

    [[ $kver =~ $re ]] || return 1

    printf '%s' "$kver"
}

plain() {
    local mesg=$1; shift
    printf "    $_color_bold$mesg$_color_none\n" "$@" >&1
}

quiet() {
    (( _optquiet )) || plain "$@"
}

msg() {
    local mesg=$1; shift
    printf "$_color_green==>$_color_none $_color_bold$mesg$_color_none\n" "$@" >&1
}

msg2() {
    local mesg=$1; shift
    printf "  $_color_blue->$_color_none $_color_bold$mesg$_color_none\n" "$@" >&1
}

warning() {
    local mesg=$1; shift
    printf "$_color_yellow==> WARNING:$_color_none $_color_bold$mesg$_color_none\n" "$@" >&2
}

error() {
    local mesg=$1; shift
    printf "$_color_red==> ERROR:$_color_none $_color_bold$mesg$_color_none\n" "$@" >&2
    return 1
}

die() {
    error "$@"
    cleanup 1
}

map() {
    local r=0
    for _ in "${@:2}"; do
        "$1" "$_" || (( $# > 255 ? r=1 : ++r ))
    done
    return $r
}

arrayize_config() {
    set -f
    [[ ${MODULES@a} != *a* ]] && MODULES=($MODULES)
    [[ ${BINARIES@a} != *a* ]] && BINARIES=($BINARIES)
    [[ ${FILES@a} != *a* ]] && FILES=($FILES)
    [[ ${HOOKS@a} != *a* ]] && HOOKS=($HOOKS)
    [[ ${COMPRESSION_OPTIONS@a} != *a* ]] && COMPRESSION_OPTIONS=($COMPRESSION_OPTIONS)
    set +f
}

in_array() {
    # Search for an element in an array.
    #   $1: needle
    #   ${@:2}: haystack

    local item= needle=$1; shift

    for item in "$@"; do
        [[ $item = $needle ]] && return 0 # Found
    done
    return 1 # Not Found
}

index_of() {
    # get the array index of an item. sets the global var _idx with
    # index and returns 0 if found, otherwise returns 1.
    local item=$1; shift

    for (( _idx=1; _idx <= $#; _idx++ )); do
        if [[ $item = ${!_idx} ]]; then
            (( --_idx ))
            return 0
        fi
    done

    # not found
    unset _idx
    return 1
}

funcgrep() {
    awk -v funcmatch="$1" '
        /^[[:space:]]*[[:alnum:]_]+[[:space:]]*\([[:space:]]*\)/ {
            match($1, funcmatch)
            print substr($1, RSTART, RLENGTH)
        }' "$2"
}

list_hookpoints() {
    local funcs script

    script=$(PATH=$_d_hooks type -P "$1") || return 0

    mapfile -t funcs < <(funcgrep '^run_[[:alnum:]_]+' "$script")

    echo
    msg "This hook has runtime scripts:"
    in_array run_earlyhook "${funcs[@]}" && msg2 "early hook"
    in_array run_hook "${funcs[@]}" && msg2 "pre-mount hook"
    in_array run_latehook "${funcs[@]}" && msg2 "post-mount hook"
    in_array run_cleanuphook "${funcs[@]}" && msg2 "cleanup hook"
}

modprobe() {
    command modprobe -d "$_optmoduleroot" -S "$KERNELVERSION" "$@"
}

auto_modules() {
    # Perform auto detection of modules via sysfs.

    local mods=

    mapfile -t mods < <(find /sys/devices -name uevent \
        -exec sort -u {} + | awk -F= '$1 == "MODALIAS" && !_[$0]++')
    mapfile -t mods < <(modprobe -qaR "${mods[@]#MODALIAS=}")

    (( ${#mods[*]} )) && printf "%s\n" "${mods[@]//-/_}"
}

all_modules() {
    # Add modules to the initcpio, filtered by grep.
    #   $@: filter arguments to grep
    #   -f FILTER: ERE to filter found modules

    local -i count=0
    local mod= OPTIND= OPTARG= filter=()

    while getopts ':f:' flag; do
        case $flag in f) filter+=("$OPTARG") ;; esac
    done
    shift $(( OPTIND - 1 ))

    while read -r -d '' mod; do
        (( ++count ))

        for f in "${filter[@]}"; do
            [[ $mod =~ $f ]] && continue 2
        done

        mod=${mod##*/}
        mod="${mod%.ko*}"
        printf '%s\n' "${mod//-/_}"
    done < <(find "$_d_kmoduledir" -name '*.ko*' -print0 2>/dev/null | grep -EZz "$@")

    (( count ))
}

add_all_modules() {
    # Add modules to the initcpio.
    #   $@: arguments to all_modules

    local mod mods

    mapfile -t mods < <(all_modules "$@")
    map add_module "${mods[@]}"

    return $(( !${#mods[*]} ))
}

add_checked_modules() {
    # Add modules to the initcpio, filtered by the list of autodetected
    # modules.
    #   $@: arguments to all_modules

    local mod mods

    if (( ${#_autodetect_cache[*]} )); then
        mapfile -t mods < <(all_modules "$@" | grep -xFf <(printf '%s\n' "${!_autodetect_cache[@]}"))
    else
        mapfile -t mods < <(all_modules "$@")
    fi

    map add_module "${mods[@]}"

    return $(( !${#mods[*]} ))
}

add_firmware() {
    # add a firmware file to the image.
    #   $1: firmware path fragment

    local fw fwpath r=1

    for fw; do
        for fwpath in "${_d_firmware[@]}"; do
            if [[ -f $fwpath/$fw ]]; then
                add_file "$fwpath/$fw" "$fwpath/$fw" 644 && r=0
                break
            fi
        done
    done

    return $r
}

add_module() {
    # Add a kernel module to the initcpio image. Dependencies will be
    # discovered and added.
    #   $1: module name

    local target= module= softdeps= deps= field= value= firmware=()
    local ign_errors=0 found=0

    [[ $KERNELVERSION == none ]] && return 0

    if [[ $1 = *\? ]]; then
        ign_errors=1
        set -- "${1%?}"
    fi

    target=${1%.ko*} target=${target//-/_}

    # skip expensive stuff if this module has already been added
    (( _addedmodules["$target"] == 1 )) && return

    while IFS=':= ' read -r -d '' field value; do
        case "$field" in
            filename)
                # Only add modules with filenames that look like paths (e.g.
                # it might be reported as "(builtin)"). We'll defer actually
                # checking whether or not the file exists -- any errors can be
                # handled during module install time.
                if [[ $value = /* ]]; then
                    found=1
                    module=${value##*/} module=${module%.ko*}
                    quiet "adding module: %s (%s)" "$module" "$value"
                    _modpaths["$value"]=1
                    _addedmodules["${module//-/_}"]=1
                fi
                ;;
            depends)
                IFS=',' read -r -a deps <<< "$value"
                map add_module "${deps[@]}"
                ;;
            firmware)
                firmware+=("$value")
                ;;
            softdep)
                read -ra softdeps <<<"$value"
                for module in "${softdeps[@]}"; do
                    [[ $module == *: ]] && continue
                    add_module "$module?"
                done
                ;;
        esac
    done < <(modinfo -b "$_optmoduleroot" -k "$KERNELVERSION" -0 "$target" 2>/dev/null)

    if (( !found )); then
        (( ign_errors || _addedmodules["$target"] )) && return 0
        error "module not found: \`%s'" "$target"
        return 1
    fi

    if (( ${#firmware[*]} )); then
        add_firmware "${firmware[@]}" ||
            warning 'Possibly missing firmware for module: %s' "$target"
    fi

    # handle module quirks
    case $target in
        fat)
            add_module "nls_cp437?"
            add_module "nls_iso8859-1?"
            ;;
        ocfs2)
            add_module "configfs?"
            ;;
        btrfs)
            add_module "libcrc32c?"
            ;;
        f2fs)
            add_module "crypto-crc32?"
            ;;
        ext4)
            add_module "crypto-crc32c?"
            ;;
    esac
}

add_full_dir() {
    # Add a directory and all its contents, recursively, to the initcpio image.
    # No parsing is performed and the contents of the directory is added as is.
    #   $1: path to directory
    #   $2: glob pattern to filter file additions (optional)
    #   $3: path prefix that will be stripped off from the image path (optional)

    local f= filter=${2:-*} strip_prefix=$3

    if [[ -n $1 && -d $1 ]]; then
        add_dir "$1"

        for f in "$1"/*; do
            if [[ -L $f ]]; then
                if [[ $f = $filter ]]; then
                    add_symlink "${f#$strip_prefix}" "$(readlink "$f")"
                fi
            elif [[ -d $f ]]; then
                add_full_dir "$f" "$filter" "$strip_prefix"
            elif [[ -f $f ]]; then
                if [[ $f = $filter ]]; then
                    add_file "$f" "${f#$strip_prefix}"
                fi
            fi
        done
    fi
}

add_dir() {
    # add a directory (with parents) to $BUILDROOT
    #   $1: pathname on initcpio
    #   $2: mode (optional)

    if [[ -z $1 || $1 != /?* ]]; then
        return 1
    fi

    local path=$1 mode=${2:-755}

    if [[ -d $BUILDROOT$1 ]]; then
        # ignore dir already exists
        return 0
    fi

    quiet "adding dir: %s" "$path"
    command install -dm$mode "$BUILDROOT$path"
}

add_symlink() {
    # Add a symlink to the initcpio image. There is no checking done
    # to ensure that the target of the symlink exists.
    #   $1: pathname of symlink on image
    #   $2: absolute path to target of symlink (optional, can be read from $1)

    local name=$1 target=$2

    (( $# == 1 || $# == 2 )) || return 1

    if [[ -z $target ]]; then
        target=$(readlink -f "$name")
        if [[ -z $target ]]; then
            error 'invalid symlink: %s' "$name"
            return 1
        fi
    fi

    add_dir "${name%/*}"

    if [[ -L $BUILDROOT$1 ]]; then
        quiet "overwriting symlink %s -> %s" "$name" "$target"
    else
        quiet "adding symlink: %s -> %s" "$name" "$target"
    fi
    ln -sfn "$target" "$BUILDROOT$name"
}

add_file() {
    # Add a plain file to the initcpio image. No parsing is performed and only
    # the singular file is added.
    #   $1: path to file
    #   $2: destination on initcpio (optional, defaults to same as source)
    #   $3: mode

    (( $# )) || return 1

    # determine source and destination
    local src=$1 dest=${2:-$1} mode=

    if [[ ! -f $src ]]; then
        error "file not found: \`%s'" "$src"
        return 1
    fi

    mode=${3:-$(stat -c %a "$src")}
    if [[ -z $mode ]]; then
        error "failed to stat file: \`%s'." "$src"
        return 1
    fi

    if [[ -e $BUILDROOT$dest ]]; then
        quiet "overwriting file: %s" "$dest"
    else
        quiet "adding file: %s" "$dest"
    fi
    command install -Dm$mode "$src" "$BUILDROOT$dest"
}

add_runscript() {
    # Adds a runtime script to the initcpio image. The name is derived from the
    # script which calls it as the basename of the caller.

    local funcs fn script hookname=${BASH_SOURCE[1]##*/}

    if ! script=$(PATH=$_d_hooks type -P "$hookname"); then
        error "runtime script for \`%s' not found" "$hookname"
        return
    fi

    add_file "$script" "/hooks/$hookname" 755

    mapfile -t funcs < <(funcgrep '^run_[[:alnum:]_]+' "$script")

    for fn in "${funcs[@]}"; do
        case $fn in
            run_earlyhook)
                _runhooks['early']+=" $hookname"
                ;;
            run_hook)
                _runhooks['hooks']+=" $hookname"
                ;;
            run_latehook)
                _runhooks['late']+=" $hookname"
                ;;
            run_cleanuphook)
                _runhooks['cleanup']="$hookname ${_runhooks['cleanup']}"
                ;;
        esac
    done
}

add_binary() {
    # Add a binary file to the initcpio image. library dependencies will
    # be discovered and added.
    #   $1: path to binary
    #   $2: destination on initcpio (optional, defaults to same as source)

    local -a sodeps
    local line= regex= binary= dest= mode= sodep= resolved=

    if [[ ${1:0:1} != '/' ]]; then
        binary=$(type -P "$1")
    else
        binary=$1
    fi

    if [[ ! -f $binary ]]; then
        error "file not found: \`%s'" "$1"
        return 1
    fi

    dest=${2:-$binary}
    mode=$(stat -c %a "$binary")

    # always add the binary itself
    add_file "$binary" "$dest" "$mode"

    # negate this so that the RETURN trap is not fired on non-binaries
    ! lddout=$(ldd "$binary" 2>/dev/null) && return 0

    # resolve sodeps
    regex='^(|.+ )(/.+) \(0x[a-fA-F0-9]+\)'
    while read -r line; do
        if [[ $line =~ $regex ]]; then
            sodep=${BASH_REMATCH[2]}
        elif [[ $line = *'not found' ]]; then
            error "binary dependency \`%s' not found for \`%s'" "${line%% *}" "$1"
            (( ++_builderrors ))
            continue
        fi

        if [[ -f $sodep && ! -e $BUILDROOT$sodep ]]; then
            add_file "$sodep" "$sodep" "$(stat -Lc %a "$sodep")"
        fi
    done <<< "$lddout"

    return 0
}

parse_config() {
    # parse key global variables set by the config file.

    map add_module "${MODULES[@]}"
    map add_binary "${BINARIES[@]}"
    map add_file "${FILES[@]}"

    tee "$BUILDROOT/buildconfig" < "$1" | {
        # When MODULES is not an array (but instead implicitly converted at
        # startup), sourcing the config causes the string value of MODULES
        # to be assigned as MODULES[0]. Avoid this by explicitly unsetting
        # MODULES before re-sourcing the config.
        unset MODULES

        . /dev/stdin

        # arrayize MODULES if necessary.
        [[ ${MODULES@a} != *a* ]] && read -ra MODULES <<<"${MODULES//-/_}"

        for mod in "${MODULES[@]%\?}"; do
            mod=${mod//-/_}
            # only add real modules (2 == builtin)
            (( _addedmodules["$mod"] == 1 )) && add+=("$mod")
        done
        (( ${#add[*]} )) && printf 'MODULES="%s"\n' "${add[*]}"

        printf '%s="%s"\n' \
            'EARLYHOOKS' "${_runhooks['early']# }" \
            'HOOKS' "${_runhooks['hooks']# }" \
            'LATEHOOKS' "${_runhooks['late']# }" \
            'CLEANUPHOOKS' "${_runhooks['cleanup']% }"
    } >"$BUILDROOT/config"
}

initialize_buildroot() {
    # creates a temporary directory for the buildroot and initialize it with a
    # basic set of necessary directories and symlinks

    local workdir= kernver=$1 arch=$(uname -m) buildroot

    if ! workdir=$(mktemp -d --tmpdir mkinitcpio.XXXXXX); then
        error 'Failed to create temporary working directory in %s' "${TMPDIR:-/tmp}"
        return 1
    fi
    buildroot=${2:-$workdir/root}

    if [[ ! -w ${2:-$workdir} ]]; then
        error 'Unable to write to build root: %s' "$buildroot"
        return 1
    fi

    # base directory structure
    install -dm755 "$buildroot"/{new_root,proc,sys,dev,run,tmp,var,etc,usr/{local,lib,bin}}
    ln -s "usr/lib" "$buildroot/lib"
    ln -s "../lib"  "$buildroot/usr/local/lib"
    ln -s "bin"     "$buildroot/usr/sbin"
    ln -s "usr/bin" "$buildroot/bin"
    ln -s "usr/bin" "$buildroot/sbin"
    ln -s "../bin"  "$buildroot/usr/local/bin"
    ln -s "../bin"  "$buildroot/usr/local/sbin"
    ln -s "/run"    "$buildroot/var/run"

    case $arch in
        x86_64)
            ln -s "lib"     "$buildroot/usr/lib64"
            ln -s "usr/lib" "$buildroot/lib64"
            ;;
    esac

    # mkinitcpio version stamp
    printf '%s' "$version" >"$buildroot/VERSION"

    # kernel module dir
    [[ $kernver != none ]] && install -dm755 "$buildroot/usr/lib/modules/$kernver/kernel"

    # mount tables
    ln -s /proc/self/mounts "$buildroot/etc/mtab"
    >"$buildroot/etc/fstab"

    # indicate that this is an initramfs
    >"$buildroot/etc/initrd-release"

    # add a blank ld.so.conf to keep ldconfig happy
    >"$buildroot/etc/ld.so.conf"

    printf '%s' "$workdir"
}

run_build_hook() {
    local hook=$1 script= resolved=
    local MODULES=() BINARIES=() FILES=() SCRIPT=

    # find script in install dirs
    if ! script=$(PATH=$_d_install type -P "$hook"); then
        error "Hook '$hook' cannot be found"
        return 1
    fi

    # check for deprecation
    if resolved=$(readlink "$script") && [[ ${script##*/} != "${resolved##*/}" ]]; then
        warning "Hook '%s' is deprecated. Replace it with '%s' in your config" \
            "${script##*/}" "${resolved##*/}"
        script=$resolved
    fi

    # source
    unset -f build
    if ! . "$script"; then
        error 'Failed to read %s' "$script"
        return 1
    fi

    if ! declare -f build >/dev/null; then
        error 'Hook '$script' has no build function'
        return 1
    fi

    # run
    if (( _optquiet )); then
        msg2 "Running build hook: [%s]" "${script##*/}"
    else
        msg2 "Running build hook: [%s]" "$script"
    fi
    build

    # if we made it this far, return successfully. Hooks can
    # do their own error catching if it's severe enough, and
    # we already capture errors from the add_* functions.
    return 0
}

try_enable_color() {
    local colors

    if ! colors=$(tput colors 2>/dev/null); then
        warning "Failed to enable color. Check your TERM environment variable"
        return
    fi

    if (( colors > 0 )) && tput setaf 0 &>/dev/null; then
        _color_none=$(tput sgr0)
        _color_bold=$(tput bold)
        _color_blue=$_color_bold$(tput setaf 4)
        _color_green=$_color_bold$(tput setaf 2)
        _color_red=$_color_bold$(tput setaf 1)
        _color_yellow=$_color_bold$(tput setaf 3)
    fi
}

install_modules() {
    local m moduledest=$BUILDROOT/lib/modules/$KERNELVERSION
    local -a xz_comp gz_comp

    [[ $KERNELVERSION == none ]] && return 0

    if (( $# == 0 )); then
        warning "No modules were added to the image. This is probably not what you want."
        return 0
    fi

    cp "$@" "$moduledest/kernel"

    # unzip modules prior to recompression
    for m in "$@"; do
        case $m in
            *.xz)
                xz_comp+=("$moduledest/kernel/${m##*/}")
                ;;
            *.gz)
                gz_comp+=("$moduledest/kernel/${m##*/}")
                ;;
        esac
    done
    (( ${#xz_comp[*]} )) && xz -d "${xz_comp[@]}"
    (( ${#gz_comp[*]} )) && gzip -d "${gz_comp[@]}"

    msg "Generating module dependencies"
    install -m644 -t "$moduledest" "$_d_kmoduledir"/modules.builtin

    # we install all modules into kernel/, making the .order file incorrect for
    # the module tree. munge it, so that we have an accurate index. This avoids
    # some rare and subtle issues with module loading choices when an alias
    # resolves to multiple modules, only one of which can claim a device.
    awk -F'/' '{ print "kernel/" $NF }' \
        "$_d_kmoduledir"/modules.order >"$moduledest/modules.order"

    depmod -b "$BUILDROOT" "$KERNELVERSION"

    # remove all non-binary module.* files (except devname for on-demand module loading)
    rm "$moduledest"/modules.!(*.bin|devname|softdep)
}

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