#!/bin/bash
# sdk-chroot

usage()
{
    cat <<EOF
    usage: $0 [-u <user>] [-m <all|none|root|home>] [-r <SDK root path>] [<command> <args> ..]
           $0 -h

       This is the minimal, chroot-based Sailfish SDK.
       For information see https://docs.sailfishos.org/Tools/Platform_SDK/

      If command is not present,
         used to enter the SDK and begin working. The SDK bash shell is a
         login shell. See below for .profile handling

      If command is present,
         used to execute an arbitrary command from within the SDK chroot
         environment. See below for .profile handling

      Options:

       -u  System user to link into SDK (not needed if using sudo)
       -m  Devices to bind mount from host: none, all (default)
           root, home
       -r The root of the SDK to use - normally derived from the
          pathname of $0
       -h  Show this help

      Profile

      Entering the SDK runs the user's normal .profile and any (SDK)
      system profile entries. It will not execute the host's system
      profile entries.

      The environment variable SAILFISH_SDK is set to allow .profile to
      detect the SDK.

      Hooks

      If the user specified has a .mersdkrc in their $HOME, it will be
      sourced to allow hook functions to be defined. Hooks are run as
      root. No commands should be executed immediately.

      These hooks are usually used to define symbolic links from any
      /parentroot/data type filesystems into the SDK root to setup
      system specific shared caches or filesystem layouts etc

EOF
    return 0
}

MY_GNUPGHOME_DIR='.config/SailfishSDK/gnupg'
MY_SSH_AUTH_SOCK=${SSH_AUTH_SOCK#/parentroot}
[[ $MY_SSH_AUTH_SOCK ]] && MY_SSH_AUTH_SOCK="/parentroot$MY_SSH_AUTH_SOCK"

if [[ $EUID -ne 0 ]]; then
    exec sudo SSH_AGENT_PID=${SSH_AGENT_PID:-} SSH_AUTH_SOCK=${MY_SSH_AUTH_SOCK} $0 "$@"
    echo "$0 must be run as root and sudo failed; exiting"
    exit 1
fi

if cmp -s /proc/$PPID/mountinfo /proc/self/mountinfo; then
    exec unshare -m -- "$0" "$@"
    echo "$0 must be run in private namespace and unshare failed; exiting"
    exit 1
fi

# Make sure that mountpoints in the new namespace are really unshared from the
# parental namespace. See unshare(1).
# This prevents mounts in the sdk from appearing in the parent fs.
mount --make-rslave /

# Use the SUDO value if present
user=$SUDO_USER || true;

bind_mount_root="yes";
bind_mount_home="yes";

while getopts "u:m:r:" opt; do
    case $opt in
	u ) user=$OPTARG;;
	m )
	    case $OPTARG in
		all) ;;
		home)
		    bind_mount_root="no";;
		root)
		    bind_mount_home="no";;
		none)
		    bind_mount_root="no";
		    bind_mount_home="no";;
		*)  echo "Only 'none', 'all' or 'home' are permitted for -m"
		    usage
		    exit 1;;
	    esac ;;
	r ) sdkroot=$OPTARG;;
	h|\? ) usage
            exit 1;;
	: ) echo "Option -$OPTARG requires an argument." >&2
	    usage
	    exit 1;;
	* ) usage
            exit 1;;
    esac
done
shift $(($OPTIND - 1))

if [[ -z "${sdkroot}" ]] ; then
    sdkroot=$(dirname $(readlink -f $0))
else
    sdkroot=$(readlink -f $sdkroot)
fi

if [[ ! -f ${sdkroot}/etc/MerSDK ]] ; then
    echo "${sdkroot} does not look like a Mer SDK rootfs"
    echo "if you are sure it is, you may mark it by running"
    echo "echo 'MerSDK' | sudo tee ${sdkroot}/etc/MerSDK"
    exit 1
fi

if ! [[ $(basename $(dirname $sdkroot)) == "sdks" ]]; then
    echo "Non-standard SDK installation layout - cannot determine location for "
    echo "SDK targets and toolings. Expected layout is:"
    echo ""
    echo "    <path/to/SDKs...>/"
    echo "    ├── sdks/"
    echo "    │   ├── <sdk_1>/"
    echo "    │   ├── <sdk_2>/"
    echo "    │   └── .../"
    echo "    ├── targets/"
    echo "    │   ├── <targets_1>/"
    echo "    │   ├── <targets_2>/"
    echo "    │   └── .../"
    echo "    └── toolings/"
    echo "        ├── <tooling_1>/"
    echo "        ├── <tooling_2>/"
    echo "        └── .../"
    exit 1
fi

targetsdir=$(readlink -f ${sdkroot}/../../targets)
if [[ ! -d ${targetsdir} ]] ; then
    echo "SDK targets location '$targetsdir' does not exist - about to create it."
    read -p "Continue? [y/n] (y)" reply
    if ! [[ $reply =~ ^[cCyY]?$ ]]; then
        echo "User aborted"
        exit 1
    fi

    mkdir "$targetsdir" || exit
fi

toolingsdir=$(readlink -f ${sdkroot}/../../toolings)
if [[ ! -d ${toolingsdir} ]] ; then
    echo "SDK toolings location '$toolingsdir' does not exist - about to create it."
    read -p "Continue? [y/n] (y)" reply
    if ! [[ $reply =~ ^[cCyY]?$ ]]; then
        echo "User aborted"
        exit 1
    fi

    mkdir "$toolingsdir" || exit
fi

if [[ -z $user ]] ; then
    echo "$0 expects to be run as root using sudo"
    echo "User could not be obtained from \$SUDO_USER, if running as root,"
    echo "please use -u <user>"
    echo
    usage
    exit 1
fi

# From now on, exit if variables not set
set -u

# Make sure normal users can use any dirs we make
umask 022

################################################################
# Mount

# In order to deal with varying status of adoption of changes to FHS among
# distributions a list of alternate paths is considered for binding.
mount_bind() {
    maybe_symlinks="$*"
    src=""
    dsts=""

    for dir in $maybe_symlinks; do
        if [[ -d $dir ]]; then
            src="$dir"
            break
        fi
    done

    if [[ -z "$src" ]]; then
        echo "mount_bind $*: None of these exists on your host - please report this bug"
        return
    fi

    for dir in $maybe_symlinks; do
        if [[ -e ${sdkroot}$dir && ! -L ${sdkroot}$dir ]]; then
            dsts="$dsts $dir"
        fi
    done

    if [[ -z "$dsts" ]]; then
        echo "mount_bind $*: No non-symlink target in SDK root - please report this bug"
        return
    fi

    for dst in $dsts; do
        mount --bind $src ${sdkroot}$dst
    done
}
prepare_mountpoints() {
    # This prevents the following mount_bind to hang first time after reboot.
    # Bug in binfmt_misc pseudo-filesystem?
    ls /proc/sys/fs/binfmt_misc -a > /dev/null

    echo "Mounting system directories..."
    mount_bind /proc
    mount_bind /proc/sys/fs/binfmt_misc
    mount_bind /sys
    mount_bind /dev
    mount_bind /dev/pts
    mount_bind /dev/shm /run/shm

    echo "Mounting $targetsdir as /srv/mer/targets"
    mkdir -p ${sdkroot}/srv/mer/targets
    mount --rbind ${targetsdir} ${sdkroot}/srv/mer/targets/

    echo "Mounting $toolingsdir as /srv/mer/toolings"
    mkdir -p ${sdkroot}/srv/mer/toolings
    mount --rbind ${toolingsdir} ${sdkroot}/srv/mer/toolings/

    if [[ $bind_mount_root == "yes" ]] ; then
	echo "Mounting / as /parentroot"
	mkdir -p ${sdkroot}/parentroot
	mount --rbind / ${sdkroot}/parentroot/
    fi

    mkdir -p ${sdkroot}/lib/modules/`uname -r`
    mount_bind /lib/modules/`uname -r`

}

# Replace 'shell' field in `getent passwd` output with /bin/bash if the shell
# is not available in the chroot
fix_shell() {
    local shell= other=
    while IFS=: read shell other; do
        if ! [[ -e ${sdkroot}${shell} ]]; then
            shell=/bin/bash
        fi

        echo "$other:$shell"
    done <<<"$(awk -F: -v OFS=: '{print $7,$1,$2,$3,$4,$5,$6}')"
}

# Meet the requirements of sudo
fix_password() {
    awk -F: -v OFS=: '{$2="!"; print}'
}

prepare_user() {
    # remove mer user if present
    sed -i -e "/^mer:/d" ${sdkroot}/etc/passwd
    # getent is probably best for user data
    sed -i -e "/^${user}:/d" ${sdkroot}/etc/passwd
    getent passwd $user |fix_shell |fix_password >> ${sdkroot}/etc/passwd
    group=$(getent passwd $user | cut -f4 -d:)
    sed -i -e "/^[^:]*:[^:]*:${group}:/d" ${sdkroot}/etc/group
    getent group $group >> ${sdkroot}/etc/group
    HOMEDIR=$(getent passwd $user | cut -f6 -d:)

    install --owner $user --group $(id -gn $user) -d ${sdkroot}${HOMEDIR}

    if [[ $bind_mount_home == "yes" ]] ; then
	echo "Mounting home directory: ${HOMEDIR}"
	mount --bind ${HOMEDIR} ${sdkroot}${HOMEDIR}
	# Now the sdk uses a private namespace, there's no need to
	# make it unbindable
	mount --make-shared ${sdkroot}${HOMEDIR}
        # Prevent accidental writes to host's GPG home (SDK uses its own GPG home)
        if [[ -d ${sdkroot}${HOMEDIR}/.gnupg ]]; then
            mount --bind -o ro "${sdkroot}${HOMEDIR}/.gnupg"{,}
        fi
    fi

    sudo -u $user mkdir -p --mode 700 "${sdkroot}${HOMEDIR}/${MY_GNUPGHOME_DIR}"

    echo "$user ALL=NOPASSWD: ALL" > ${sdkroot}/etc/sudoers.d/sdk
    chmod 0440 ${sdkroot}/etc/sudoers.d/sdk
}

prepare_etc() {
    # Symlink to parentroot to support dynamic resolv.conf on host
    rm -f ${sdkroot}/etc/resolv.conf
    resolv=$(readlink -fn /etc/resolv.conf) # some systems use symlinks to /var/run/...
    ln -s /parentroot/$resolv ${sdkroot}/etc/resolv.conf

    # Fixup old SDKs with broken /etc/mtab since this won't be fixed
    # by any package updates
    if [[ ! -L ${sdkroot}/etc/mtab ]]; then
	echo "The /etc/mtab file in the SDK is not a symbolic link - forcing it to link to /proc/self/mounts to fix https://bugs.merproject.org/show_bug.cgi?id=385"
	rm -f ${sdkroot}/etc/mtab
	ln -s /proc/self/mounts ${sdkroot}/etc/mtab
    fi

}

ensure_machine_id() {
    setarch i386 chroot ${sdkroot} /usr/bin/systemd-machine-id-setup
    setarch i386 chroot ${sdkroot} /usr/bin/dbus-uuidgen --ensure
}

################

setup_user_hooks(){
    # Access any user hooks
    [[ -e $HOMEDIR/.mersdkrc ]] && . $HOMEDIR/.mersdkrc
}

run_user_hook() {
    hook=$1
    [[ $(type -t $hook) == "function" ]] && {
	echo "User hook $hook"
	$hook
    }
}
################

bash_command() {
    cmd=("$@")
    setarch i386 chroot ${sdkroot} /bin/su -s /bin/bash -l $user \
       "${common_whitelist_environment[@]/#/--whitelist-environment=}" \
       -- -c "
[[ -d ${cwd@Q} ]] && cd ${cwd@Q}

sudo oneshot

exec ${cmd[*]@Q}
"
}

bash_interactive_shell() {
    echo "Entering chroot as $user"
    export SAILFISH_SDK_CWD=$cwd

    setarch i386 chroot ${sdkroot} /bin/su -s /bin/bash -l $user \
       "${common_whitelist_environment[@]/#/--whitelist-environment=}" \
       --whitelist-environment=SAILFISH_SDK_CWD
}

################################################################

retval=0
cwd=$(pwd)

[[ $0 = *mer-sdk-chroot ]] && echo "WARNING: mer-sdk-chroot is deprecated. Use sdk-chroot instead."

# For back compatibility abort on 'mount/umount' and warn on 'enter' 
if [[ $# == 1 ]]; then
    case $1 in
	mount|umount )
	    cat <<EOF
ERROR: the sdk-chroot command no longer needs or supports 'mount/umount/enter'
Just enter the SDK using:
 $0

SDK mount/umount is handled automatically and safely.
EOF
	    exit 1;;
	enter )
	    cat <<EOF
WARNING: sdk 'enter' is deprecated. Just enter the SDK using:
  $0

Entering the SDK as requested
EOF
	    shift;;
    esac
fi

prepare_mountpoints   # host / and data and /proc and similar
prepare_user          # in /etc/passwd
setup_user_hooks      # (after prepare so HOMEDIR is known)
prepare_etc           # resolv.conf and ssl certs
ensure_machine_id
run_user_hook mount_sdk
run_user_hook enter_sdk
trap 'rv=$?; run_user_hook leave_sdk; exit "$rv"' EXIT

if [[ ${1:-} == 'exec' ]]; then
	cat <<EOF
WARN: sdk 'exec' is deprecated. Just execute SDK commands using:
	$0 <cmd> <args>

Executing commands as requested
EOF
	shift # Remove the offending 'exec'

	if ! [[ $1 ]]; then
		echo "You must supply a command to exec"
	    usage
		exit 1
	fi
fi

export SSH_AUTH_SOCK=${MY_SSH_AUTH_SOCK}
export SSH_AGENT_PID=${SSH_AGENT_PID:-}
export MERSDK=$sdkroot
export SAILFISH_SDK=$sdkroot
export GNUPGHOME=${HOMEDIR}/${MY_GNUPGHOME_DIR}
common_whitelist_environment=(
    SSH_AUTH_SOCK SSH_AGENT_PID
    MERSDK SAILFISH_SDK
    GNUPGHOME
)

if [[ $# -gt 0 ]]; then
    bash_command "$@"
else
    bash_interactive_shell
fi
