#!/bin/bash
# Copyright 2015 Calculate Ltd. http://www.calculate-linux.org
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

PATH=/lib/rc/bin:$PATH

CL_KERNEL_VERSION=0.1_alpha3
DESCRIPTION="Create kernel utility"
DEFAULT_KERNEL_DIRECTORY=/usr/src/linux
SRC_DIRECTORY=/usr/src
LOCAL_TEMPLATES_DIR=/var/calculate/templates/kernel
TEMPLATES_BACKUP=${LOCAL_TEMPLATES_DIR}/backup
TEMPLATE_NAME_PREFIX=10-config-
DEBUG_LOG=/var/log/calculate/cl-kernel.log
KVER=
KERNEL_DIR=${DEFAULT_KERNEL_DIRECTORY}
# create base config
CREATE_BASE=1
# create config by template
CREATE_NEW=1
BUILD_KERNEL=1
CHMOD=0644

: >$DEBUG_LOG
set -e

usage() {
    cat << EOF
Usage: $0 [OPTION]

Version: $CL_KERNEL_VERSION

${DESCRIPTION}

    -h, --help  Display all options
EOF
}

long_usage() {
    cat <<EOF

Usage: $0 [OPTION]

Version: $CL_KERNEL_VERSION

${DESCRIPTION}

  --kver [VERSION]           specify kernel version ('list' for displaying possible values)
  --kver-old [VERSION]       specify kernel version for new options ('list' for displaying possible values)
  --convert                  migrate .config in kernel directory to templates
  --skip-build               do not build kernel after configuration
  --march [ARCH]             kernel architecture
  --safemode                 create additional initrd with all modules (only for calculate-sources)
  --help                     display this help and exit
EOF
}

rearrange_params() {
    TEMP=$(unset POSIXLY_CORRECT; getopt \
        -o "h" \
        --long help \
        --long kver: \
        --long kver-old: \
        --long march: \
        --long convert \
        --long skip-build \
        --long safemode \
        -- "$@")
    if (( $? != 0 )); then
        usage
        exit 1
    fi
}

do_args() {
    while :; do
        case $1 in
        -h|--help)
            long_usage
            exit 0
            ;;
        --kver)
            KVER="$2"
            shift
            ;;
        --kver-old)
            KVER_OLD="$2"
            shift
            ;;
        --safemode)
            SAFEMODE=1
            ;;
        --march)
            MARCH="$2"
            if [[ ${MARCH} != "x86" ]] && [[ ${MARCH} != "x86_64" ]]
            then
                eerror "Error in parameter --march. The value may be 'x86' or 'x86_64'"
            fi
            shift
            ;;
        --convert)
            MIGRATE=1
            CREATE_NEW=
            ;;
        --skip-build)
            BUILD_KERNEL=
            ;;
        --) shift; break;;
        *) 
            usage;
            eerror "Unknown option: $1"
       esac
       shift
    done
}

source_makeconf() {
    [[ -f /etc/make.conf ]] && source /etc/make.conf
    if [[ -d /etc/portage/make.conf ]]
    then
        for makeconf in /etc/portage/make.conf/*
        do
            source $makeconf
        done
    fi
}

variable_value()
{
    local varname=$1
    cl-core-variables-show --only-value $varname
}

# оставить только названия параметров + "="
options_name() {
    sed -r 's/^(.*=)(.*)$/\1/'
}

# преобразовать список строк в параметры для grep
grep_patterns() {
    awk '{print  "-e "$1}'
}

# исключить из вывода переданные шаблоны
exclude_options() {
    if [[ -n $1 ]]
    then
        grep -v $*
    else
        cat
    fi
}

# сохранить в выводе переданные шаблоны
keep_options() {
    if [[ -n $1 ]]
    then
        grep $*
    else
        cat
    fi
}

# преобразовать опции в синтаксис удаления параметра
remove_syntax() {
    sed -r 's/^(.*=.*)$/!\1/'
}

# удалить выключенные параметры
discard_option_n() {
    grep -v "=n$"
}

# оставить только выключенные параметры
keep_option_n() {
    grep "=n$"
}

# преобразовать # CONFIG_XXX is not set -> CONFIG_XXX=n
not_set2n() {
    sed -r 's/# (CONFIG.*) is not set/\1=n/'
}

# получить изменённые и новые параметры с новыми значениями
append_options() {
    diff -u $1 $2 | grep "^+[C#]" | sed 's/^.//' | not_set2n
}

# получить изменённые и удалённые параметры со старыми значениями
removed_options() {
    diff -u $1 $2 | grep "^-[C#]" | sed 's/^.//' | not_set2n
}

# получить содержимое шаблона
diff_template_body() {
    local orig_file=$1
    local new_file=$2
    # получаем список опций, которые стали выключенными в новом конфиге
    local keep_opts=( $(append_options $orig_file $new_file | keep_option_n | options_name | grep_patterns) )
    # получаем список опций, которые не изменились по значению, но возможно изменились по расположению в файле
    local exclude_opts=( $(append_options $orig_file $new_file | grep_patterns) )
    # сохраняем в шаблоне старые значения опций, которые изменились с пометкой на удаление
    removed_options $orig_file $new_file | exclude_options ${exclude_opts[*]} | keep_options ${keep_opts[*]} | remove_syntax | sort || true
    # получаем список опций, которые изменились
    local exclude_opts=( $(removed_options $orig_file $new_file | options_name | grep_patterns) )
    # сохраняем список опций, которые стали выключенные, но в отсутствовали в старом конфиге (оставляем только новые опции)
    append_options $orig_file $new_file | exclude_options ${exclude_opts[*]} | keep_option_n | remove_syntax | sort || true
    # получаем список опций, которые не изменились по значению, но возможно изменились по расположению в файле
    local exclude_opts=( $(removed_options $orig_file $new_file | grep_patterns) )
    # сохраняем список опций которые изменились и стали либо y либо m
    append_options $orig_file $new_file | exclude_options ${exclude_opts[*]} | discard_option_n | sort || true
}

diff_template_head() {
    local category_pn=$1
    local pv=$2
    echo "# Calculate format=kernel name=.config os_install_arch_machine==${TEMPLATE_ARCH}&&merge(${category_pn})>=${pv}"
}

variable_value()
{
    local varname=$1
    /usr/sbin/cl-core-variables-show --only-value $varname
}

create_template() {
    local category_pn=$1
    local pv=$2
    local base_config=$3
    local new_config=$4
    diff_template_head ${category_pn} ${pv}
    diff_template_body ${base_config} ${new_config}
}

create_kernel_config() {
    local kernel_dir=$1
    local category_pn=( ${2/\// } )
    local category=${category_pn[0]}
    local pn=${category_pn[1]}
    local pv=$3
    local tempdir=$(${MKTEMP} -d)
    local config=$(${MKTEMP})
    [[ -n $4 ]] && local templates="-T $4"
    (cd ${kernel_dir};ARCH=$MARCH KCONFIG_CONFIG=.config_clkernel make defconfig;mv .config_clkernel ${tempdir}/.config) &>>$DEBUG_LOG ||
    	eerror "Failed to create default kernel config"
    /usr/sbin/cl-core-patch --march=$TEMPLATE_ARCH --pkg-name ${pn} --pkg-category ${category} --pkg-version=${pv} --pkg-slot=${pv} --pkg-path=$tempdir $templates &>>$DEBUG_LOG || eerror "Failed to apply kernel templates"
    cat $tempdir/.config || eerror "Kernel configuration file not found"
    rm -rf $tempdir &>>$DEBUG_LOG
}

check_kernel_sources() {
    local sources_dir=$1
    [[ -f ${sources_dir}/arch/x86/configs/i386_defconfig ]]
}

list_kernel() {
    for f in "${EROOT}"/usr/src/linux-[[:digit:]]*; do
                check_kernel_sources $f && einfo $(basename "${f}" | sed 's/linux-//')
    done
}

#####################
# Process options
#####################
rearrange_params "$@"
eval set -- "$TEMP"
do_args "$@"

######################
# 
######################
[[ -z $MARCH ]] && MARCH=$(/usr/bin/arch)
if [[ "$MARCH" == "x86" ]] || [[ "$MARCH" == "i686" ]]
then
    MARCH=i386
    TEMPLATE_ARCH=i686
    NAME_ARCH=x86
else
    TEMPLATE_ARCH=$MARCH
    NAME_ARCH=$MARCH
fi

TMP_TEMPLATE=/tmp/cl_kernel_${MARCH}
MKTEMP="/usr/bin/mktemp ${TMP_TEMPLATE}.XXXXXX"
rm -rf ${TMP_TEMPLATE}*

# пропустить сборку ядра если выбранная архитектура и архитектура машины отличаются
if [[ $TEMPLATE_ARCH != $(/usr/bin/arch) ]]
then
    OTHER_ARCH=1
    BUILD_KERNEL=
fi

if [[ $KVER == "list" ]] || [[ $KVER_OLD == "list" ]]
then
    list_kernel
    exit 0
fi

if [[ -n $KVER ]]
then
    KERNEL_DIR=${SRC_DIRECTORY}/linux-${KVER}
fi
if [[ -n $KVER_OLD ]]
then
    KERNEL_OLD_DIR=${SRC_DIRECTORY}/linux-${KVER_OLD}
fi
for check_dir in ${KERNEL_DIR} ${KERNEL_OLD_DIR}
do
    [[ -d ${check_dir} ]] || eerror "Kernel directory ${check_dir} not found"
    check_kernel_sources  ${check_dir} || eerror "Kernel directory ${check_dir} has not full sources"
done 

if [[ -z $KVER ]]
then
    KVER=$(basename "$(readlink $KERNEL_DIR)" | sed 's/linux-//')
fi

if [[ -n $SAFEMODE ]] && ! [[ $KVER =~ -calculate ]]
then
    eerror "--safemode available for calculate-sources only"
fi

if ! [[ -d $LOCAL_TEMPLATES_DIR ]]
then
    (mkdir -p $LOCAL_TEMPLATES_DIR ;
    echo "# Calculate env=install cl_ver>=3.3.0 cl_name==calculate-core&&ac_install_patch==on append=skip" >${LOCAL_TEMPLATES_DIR}/.calculate_directory) ||
        eerror "Failed to create local kernel template directory"
fi

if [[ -n $OTHER_ARCH ]]
then
    NEW_CONFIG=${KERNEL_DIR}/.config_${TEMPLATE_ARCH}
else
    NEW_CONFIG=${KERNEL_DIR}/.config
fi

CATEGORY_PN=$( qfile -C ${KERNEL_DIR}/Makefile | awk '{print $1}' )
PV=$( qfile -Cv ${KERNEL_DIR}/Makefile | awk '{print $1}' )
PV=${PV/$CATEGORY_PN-/}

if [[ -n $KERNEL_OLD_DIR ]] 
then
    CATEGORY_PN_OLD=$( qfile -C ${KERNEL_OLD_DIR}/Makefile | awk '{print $1}' )
    PV_OLD=$( qfile -Cv ${KERNEL_OLD_DIR}/Makefile | awk '{print $1}' )
    PV_OLD=${PV_OLD/${CATEGORY_PN_OLD}-/}
fi

[[ $KVER =~ ^([[:digit:]]+\.[[:digit:]]+) ]] && PV2=${BASH_REMATCH[0]} || PV2=$PV

TEMPLATE_NAME_ARCH_PREFIX=${TEMPLATE_NAME_PREFIX}${NAME_ARCH}-
TEMPLATE_NAME="${LOCAL_TEMPLATES_DIR}/${TEMPLATE_NAME_ARCH_PREFIX}${PV2}"

##########################
# Preparing kernel configs
##########################

if [[ -n $CREATE_NEW ]]
then
    ebegin "Prepare current kernel config"
    create_kernel_config ${KERNEL_OLD_DIR:-${KERNEL_DIR}} \
        ${CATEGORY_PN_OLD:-${CATEGORY_PN}} \
        ${PV_OLD:-${PV}} >$NEW_CONFIG
    eend
else
    ebegin "Use current kernel config"
    eend
fi

BASE_CONFIG=$( ${MKTEMP} )

ebegin "Prepare base kernel config"
if [[ -n $CREATE_BASE ]]
then
    TEMPLATES=$(variable_value main.cl_template_location)
    create_kernel_config ${KERNEL_DIR} ${CATEGORY_PN} ${PV} ${TEMPLATES/,local,remote/} >$BASE_CONFIG
else
    cp $NEW_CONFIG $BASE_CONFIG
fi
eend

###########################
# Execute nconfig
###########################
(cd $KERNEL_DIR; [[ -n ${KERNEL_OLD_DIR} ]] && KCONFIG_CONFIG=$(basename $NEW_CONFIG) make oldconfig;KCONFIG_CONFIG=$(basename $NEW_CONFIG) make -s nconfig) || true

###########################
# Creating template
###########################
NEW_TEMPLATE=$( ${MKTEMP} )
create_template $CATEGORY_PN $PV2 $BASE_CONFIG $NEW_CONFIG >${NEW_TEMPLATE}

###########################
# Create backup
###########################
if ls ${LOCAL_TEMPLATES_DIR}/${TEMPLATE_NAME_ARCH_PREFIX}* &>/dev/null
then
    for i in ${LOCAL_TEMPLATES_DIR}/${TEMPLATE_NAME_ARCH_PREFIX}*
    do
        if diff -u $i $NEW_TEMPLATE &>/dev/null
        then
            einfo "Kernel configuration is not changed"
            rm $i
        else
            newname="$(basename $i)-$(date +%Y%m%d_%H%M -r $i)"
            einfo "Backup template $(basename $i) -> ${newname}"
            if ! [[ -d ${TEMPLATES_BACKUP} ]]
            then
                (mkdir -p ${TEMPLATES_BACKUP} &&
                    echo "# Calculate cl_action==skip" >${TEMPLATES_BACKUP}/.calculate_directory) || eerror "Failed to create backup directory"
            fi
            mv  $i ${TEMPLATES_BACKUP}/${newname}
        fi
    done
fi

mv $NEW_TEMPLATE $TEMPLATE_NAME
chmod ${CHMOD} $TEMPLATE_NAME

einfo "Create template $(basename $TEMPLATE_NAME)"

rm -f $BASE_CONFIG

if [[ -n ${BUILD_KERNEL} ]]
then
    einfo $KVER
    cd $KERNEL_DIR
    (source_makeconf && make $MAKEOPTS && make $MAKEOPTS modules && make $MAKEOPTS install && make $MAKEOPTS modules_install)
    grep -q "CONFIG_BLK_DEV_INITRD=y" ${NEW_CONFIG} && dracut -fH --kver $KVER /boot/initramfs-${KVER}.img
    if [[ $KVER =~ calculate ]] && [[ -n $SAFEMODE ]]
    then
        
        grep -q "CONFIG_BLK_DEV_INITRD=y" ${NEW_CONFIG} && dracut -f --kver $KVER /boot/initramfs-${KVER/-calculate/-SafeMode-calculate}.img
    fi
    cl-setup-boot
fi

einfo "All done!"
