#!/bin/bash
#
# This is a helper script for the Sailfish SDK to manage sb2 target and
# toolchain installation

set -o nounset
shopt -s extglob
shopt -s globstar

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

SELF=$(basename "$0")
____=${SELF//?/ }

usage() {
    cat <<EOF
Try '$SELF --help' for more information.
EOF
}

help() {
    less --quit-if-one-screen <<EOF
usage:
    $SELF tooling list [-l|--long]
    $SELF tooling upgradable <name>
    $SELF tooling install <name> <url>
    $SELF tooling clone <original> <clone>
    $SELF tooling remove <name>
    $SELF tooling refresh {--all | <name>...}
    $SELF tooling update <name>
    $SELF tooling register [--user <user> [--password <password>]] [--force]
    $____                  {--all | <name>...}
    $SELF tooling package-list <name> [-l|--long] [<pattern>...]
    $SELF tooling package-install <name> <package-name>...
    $SELF tooling package-remove <name> <package-name>...
    $SELF tooling package-diff <name> <base-name>
    $SELF tooling package-cache-prune <name>
    $SELF tooling uuidgen {--all | <name>...}
    $SELF tooling maintain <name> [--no-sync] [<command> [<arg>...]]

    $SELF target list [-l|--long] [--tooling <name>] [--snapshots-of <name>]
    $____             [--no-snapshots] [--check-snapshots]
    $SELF target upgradable <name>
    $SELF target install <name> <url> [--tooling <name>
    $____                [--tooling-url <url>]] [--toolchain <name>]
    $____                [--no-snapshot]
    $SELF target snapshot [-r|--reset[={soft|outdated|force}]] [--no-sync]
    $____                 <original> <snapshot>
    $SELF target reserve [--reset-reused={soft|outdated|force}] <original>
                         <snapshot-template> <lock-file> <pool-size>
    $SELF target clone <original> <clone>
    $SELF target remove [--force] [--snapshots-of] <name>
    $SELF target refresh {--all | <name>...}
    $SELF target update <name>
    $SELF target sync <name> [<timestamp>]
    $SELF target import <name>
    $SELF target register [--user <user> [--password <password>]] [--force]
    $____                 {--all | <name>...}
    $SELF target package-list <name> [-l|--long] [<pattern>...]
    $SELF target package-install <name> <package-name>...
    $SELF target package-remove <name> <package-name>...
    $SELF target package-diff <name> <base-name>
    $SELF target package-cache-prune <name>
    $SELF target uuidgen {--all | <name>...}
    $SELF target maintain <name> [--no-sync] [<command> [<arg>...]]

    $SELF toolchain list <tooling>
    $SELF toolchain install <tooling> <name>...
    $SELF toolchain remove <tooling> <name>...

    $SELF develpkg list <target> [<search string>]
    $SELF develpkg install <target> <name>...
    $SELF develpkg remove <target> <name>...

    $SELF sdk version
    $SELF sdk refresh
    $SELF sdk upgradable
    $SELF sdk upgrade
    $SELF sdk status
    $SELF sdk register [--user <user> [--password <password>]] [--force]

    $SELF refresh-all [--no-sdk]

    $SELF register-all [--user <user> [--password <password>]] [--force] [--no-sdk]

    $SELF --version

Usage notes
    $SELF target list
        The '--long' format uses the following columns:

        TARGET NAME
        TOOLING NAME
          - or '-' if the target does not use a tooling
        MODE
          - see '--mode'
        ORIGINAL TARGET NAME
          - or '-' if this is not a snapshot
          - immediatelly followed with '*' if the snapshot is outdated and
            checking snapshots was requested with '--check-snapshots'

    $SELF target install
        When tooling selection is forced with '--tooling' without passing
        '--tooling-url', the selected tooling MUST exist.  If '--tooling-url' is
        passed as well, the tooling MAY NOT exist, in which case it will be
        installed automatically prior to target installation.

    $SELF target snapshot
        Snapshot targets are like normal targets in most respects.  When the
        '--reset[={soft|outdated|force}]' option is used, it is not an error
        when the target named <snapshot> already exists, provided that it is a
        snapshot target that was taken from the specified <original> target.
        This defaults to the 'outdated' behavior, in which case the snapshot
        will be reset only when the original target was updated (RPM database
        changed, etc.) after the snapshot was taken.  A snapshot can be reset
        unconditionally by passing 'force' instead.  When 'soft' is used,
        existing snapshot will be used as is. Resetting a snapshot has the same
        effect as removing the snapshot and creating it again, except that it
        preserves it's internal state and is more efficient. When the
        '--no-sync' option is used during initial snapshot creation, the
        snapshot will not be enabled for synchronization to host.

    $SELF target reserve
        Reserve a pooled snapshot of the target identified with <original>.
        The name for the snapshot will be printed on stdout. It will be
        determined using <snapshot-template>, which is expected to contain at
        least 3 consecutive 'X's in its last dot-separated component. The
        snapshot will be reserved for as long as the <lock-file> remains
        locked.  <pool-size> serves also as a limit for the number of
        snapshots that can be reserved. Only snapshots using the given
        <snapshot-template> are considered for this purpose. The
        least-recently-used approach is applied to choose among unused pooled
        snapshots unless a snapshot exists, which was used with the same
        <lock-file> recently. Such a snapshot is choosen preferably and reset
        according to the '--reset-reused' option (defaults to 'outdated').
        Other existing snapshots are reset forcefully.

    $SELF {tooling|target} clone
        Compared to taking a snapshot, cloning creates a new standalone tooling
        or target with no relation to the original.

    $SELF target sync <name> [<timestamp>]
        With <timestamp> passed, the target will only be synchronized only if
        the RPM database inside the target changed since that time. <timestamp>
        may be specified as seconds since the Epoch only.

Global options
    -m, --mode <mode>
        Accepts one of 'user' or 'installer'. By default the user mode is
        active. Targets and toolings installed in the installer mode cannot be
        removed in the user mode. This option is not meant to be used by
        regular users.

    --interactive
        Require confirmation and allow user intervention in some cases.

For more information see https://sailfishos.org/wiki/Platform_SDK

EOF
}

if [[ $EUID -eq 0 ]]; then
    echo >&2 "WARNING: Invoking $0 as root is deprecated."
    if [[ -n ${SUDO_USER:-} ]]; then
        exec sudo -i -u $SUDO_USER $0 "$@"
        echo >&2 "sudo as '$SUDO_USER' failed"
        exit 1
    else
        echo >&2 "Cannot determine Mer SDK user. Invoke $0 as non-root to fix this."
        exit 1
    fi
fi

################################################################'
# Common utilities

# When sb2 is used outside of home directory CWD mapping may not be possible.
# This would break e.g. `sdk-manage target install <pkg>` when invoked outside
# of home directory - zypper would fail.  The `cd` is intentionally
# unconditional, so that eventual issues are discovered ASAP.
sb2() ( cd; command sb2 "$@" )

print_array() {
    local array=("$@")
    declare -p array |sed 's/^[^=]*=//'
}

# Unpack result of `print_array` to variables passed as second and following args
unpack() {
    (eval local -a array=$1) 2>/dev/null || return
    eval local -a array=$1
    shift
    local i=0
    while [[ $# -gt 0 ]]; do
        if [[ $(declare -p "$1") == "declare -a "* ]]; then
            eval local -a array_item=${array[i++]}
            eval $1=\(\${array_item[@]:+"\${array_item[@]}"}\)
        else
            eval $1=\${array[i++]}
        fi
        shift
    done
}

# Suppress command's stderr unless it exits with non-zero
silent() {
    local stderr=
    { stderr=$("$@" 3>&1 1>&2 2>&3 3>&-); } 2>&1
    local rc=$?
    [[ $rc -eq 0 ]] || printf >&2 "%s\n" "$stderr"
    return $rc
}

assert_name_valid() {
    local name=$1
    if ! [[ $name ]]; then
        echo >&2 "Name cannot be an empty string"
        return 1
    fi
    if ! [[ $name =~ ^[[:alnum:]_.-]*$ ]]; then
        echo >&2 "Name cannot consist of non-alphanumeric characters other than '_-.': '$name'"
        return 1
    fi
    if ! [[ $name =~ ^[[:alnum:]] ]]; then
        echo >&2 "Name must start with an alphanumeric character: '$name'"
        return 1
    fi
}

# Downloads file at ${url} into ${tmp_dir} using ${local_file_hint} as file name, unless ${url}
# points to a local file already, in which case the file is to be used directly.
#
# On success prints array with following items:
#  - local file path
#  - non-empty if the file was actually downloaded
#
# Returns non-zero on failure.
download() {
    local url=$1 tmp_dir=$2 local_file_hint=$3

    local local_file= downloaded=

    url=${url#file://}

    local_file_hint+=.tar.${url##*.}

    if ! [[ $url =~ ^(http|ftp)s?:// ]]; then
        local_file=$(readlink -f "$url") || return
        print_array "$local_file" "$downloaded"
        return
    fi

    local_file=$tmp_dir/$local_file_hint
    local orig_downloaded_file=$tmp_dir/${url##*/}
    local md5file="$orig_downloaded_file.md5sum"
    local md5sum_failed=

    local suceeded=
    download_cleanup() (
        trap 'echo cleaning up...' INT TERM HUP
        rm -f "$md5file"
        if [[ ! $succeeded ]]; then
            rm -f "$orig_downloaded_file"
            rm -f "$local_file"
        fi
    )
    trap 'download_cleanup; trap - RETURN' RETURN
    trap 'return 1' INT TERM HUP

    echo >&2 "Downloading '${url##*/}'"
    curl "$url" -o "$orig_downloaded_file"
    curl_status=$?

    if [[ $curl_status -eq 0 ]] ; then
        # check if md5sum file exists
        if curl --output /dev/null --silent --head --fail "$url.md5sum"; then
            # md5sum exists, let's check it
            curl --silent "$url.md5sum" > "$md5file"
            if ! (cd "$tmp_dir" && md5sum --status -c "$md5file"); then
                echo >&2 "ERROR: md5sum check failed for file '$orig_downloaded_file'!"
                md5sum_failed=1
            else
                echo >&2 "INFO: md5sum matches - download ok"
            fi
        else
            echo >&2 "WARNING: No md5sum file found - can not verify file integrity."
        fi
    fi

    if [[ $curl_status -ne 0 || ! -f $orig_downloaded_file || $md5sum_failed ||
        $(stat -c %s "$orig_downloaded_file") -lt 10000 ]]; then
        echo >&2 "Download failed!"
        return 1
    fi

    if ! [[ $orig_downloaded_file -ef $local_file ]]; then
        mv -f "$orig_downloaded_file" "$local_file" || return
    fi

    downloaded=1

    print_array "$local_file" "$downloaded"

    succeeded=1
}

maybe_get_tarball_meta_data() {
    local file=$1
    local mime=
    mime=$(file --brief --mime-type "$file")
    if [[ $mime == application/x-7z-compressed ]]; then
        7z x -so "$file" '*.meta'
    fi
}

parse_tarball_meta_data() {
    local meta=$1
    local key=$2

    local value=
    value=$(sed -n "s/^$key=//p" <<<"$meta") || return
    [[ $value ]] || { echo >&2 "Empty or no '$key' in tarball meta data"; return 1; }

    printf '%s\n' "$value"
}

maybe_decompress_tarball() {
    local file=$1
    local mime=
    mime=$(file --brief --mime-type "$file")
    if [[ $mime == application/x-tar ]]; then
        cat "$file"
    else
        7z x -so "$file" '*.tar'
    fi
}

get_object_ssu_config() {
    local optional=
    if [[ $1 == --optional ]]; then
        optional=1
        shift
    fi

    local object=$1
    local key=$2

    local root=$(get_object_root "$object")
    local ssu_ini=$root/etc/ssu/ssu.ini

    local value=
    value=$(sed -n -e "s/^$key=//p" "$ssu_ini") || return
    [[ $optional || $value ]] || { echo >&2 "Empty or no '$key' in SSU config"; return 1; }

    printf '%s\n' "$value"
}

get_object_ssu_release() {
    local object=$1

    local brand=
    brand=$(get_object_ssu_config --optional "$object" brand) || return
    : ${brand:=-} # Older releases do not have brand set

    local release=
    release=$(get_object_ssu_config "$object" release) || return

    printf '%s/%s\n' "$brand" "$release"
}

get_object_ssu_release_from_tarball_meta_data() {
    local meta=$1

    local brand=
    brand=$(parse_tarball_meta_data "$meta" brand) || return
    local release=
    release=$(parse_tarball_meta_data "$meta" release) || return

    printf '%s/%s\n' "$brand" "$release"
}

configure_http_proxy() {
    local object=$1

    local file=/etc/pacrunner/dbus-bus-address

    local address=
    if [[ $INSIDE_CHROOT ]]; then
        address="null"
    else
        address="tcp:host=127.0.0.1,port=778"
    fi

    enter "$object" mkdir -p "$(dirname "$file")" || return
    enter "$object" tee "$file" >/dev/null <<EOF
# Written by sdk-manage
$address
EOF
}

disk_full() [[ $(stat --file-system --format '%a*%s/1024' "$1") -lt 10000 ]]

update_df_cache() {
    [[ $(systemd-detect-virt) == docker ]] && return
    sudo systemctl restart sdk-freespace.service
}

# Replace 'shell' field in `getent passwd` output with /bin/bash
fix_shell() {
    cut -d: -f1-6 |sed 's,$,:/bin/bash,'
}

manage_user_targets_repository() {
    $USER_TARGETS_REPOSITORY/manage.sh --repository "$USER_TARGETS_REPOSITORY" "$@"
}

# sudo is needed to check for reflink under toolings, where the owner is root
is_reflink_possible() {
    local src_dir=$1 dst_dir=$2

    local src_tmpfile= dst_tmpfile=
    is_reflink_possible_cleanup() {
        trap 'echo cleaning up...' INT TERM HUP
        [[ $src_tmpfile ]] && sudo rm -f "$src_tmpfile"
        [[ $dst_tmpfile ]] && sudo rm -f "$dst_tmpfile"
    }
    trap 'is_reflink_possible_cleanup; trap - RETURN' RETURN
    trap 'return 1' INT TERM HUP

    src_tmpfile=$(sudo mktemp --tmpdir="$src_dir") || return
    dst_tmpfile=$(sudo mktemp --tmpdir="$dst_dir") || return

    sudo cp --reflink=always "$src_tmpfile" "$dst_tmpfile" &>/dev/null
}

sudo_reflink_trees() {
    local src=$1 dst=$2
    local dst_existed=$([[ -e "$dst" ]] && echo 1)

    sudo cp -a --reflink=always --no-target-directory "$src" "$dst" && return

    # If destination existed, then retry, because cp may still fail e.g. when
    # type of a file changes
    [[ $dst_existed ]] || return

    echo >&2 "Removing destination and trying again..."
    sudo rm -rf "$dst"
    sudo cp -a --reflink=always --no-target-directory "$src" "$dst"
}

sudo_sync_trees() {
    local src=$1 dst=$2
    local dst_existed=$([[ -e "$dst" ]] && echo 1)

    sudo install -d --owner="$(id -u)" --group="$(id -g)" "$dst" || return

    local can_reflink=$(is_reflink_possible "$src" "$dst" && echo 1)

    if [[ $can_reflink ]]; then
        if ! sudo_reflink_trees "$src" "$dst"; then
            echo >&2 "Reflink failed, trying the hard way..."
            can_reflink=
        fi
    fi
    if [[ $dst_existed || ! $can_reflink ]]; then
        sudo rsync -a --delete "$src/" "$dst" || return
    fi
}

assert_template_valid() {
    local template=$1
    if ! [[ $template =~ \.XXXX*$ ]]; then
        echo >&2 "Expected name with at least 3 consecutive 'X's in its last component." \
            "Got '$template'"
        return 1
    fi
}

name_matches_template() {
    local name=$1 template=$2

    assert_template_valid "$template" || return

    local prefix=${template%.*}
    local suffix=${template##*.}

    [[ $name =~ ^"$prefix."${suffix//?/.}$ ]]
}

random_name() {
    local template=$1 existing_names=$(cat)

    assert_template_valid "$template" || return

    local prefix=${template%.*}
    local suffix_length=$((${#template} - ${#prefix} - 1))

    local suffix= name=
    while true; do
        suffix=$(LC_ALL=C tr -dc A-Z0-9 </dev/urandom \
            |dd bs=1 count="$suffix_length" 2>/dev/null) || return
        name=$prefix.$suffix
        if ! grep -q -F --line-regexp "$name" <<<"$existing_names"; then
            break
        fi
    done

    echo "$name"
}

is_locked() {
    local lock_file=$1
    [[ -e $lock_file ]] && ! flock --nonblock 0 <"$lock_file"
}

rpm_db_not_changed_since() {
    local dir=$1 timestamp=$2
    # Only consider the primary database files - those with capitalised names
    content_not_changed_since "$dir" "$timestamp" "[A-Z]*"
}

content_not_changed_since() {
    local dir=$1 timestamp=$2 filter=${3:-}

    local tmpfile=
    content_not_changed_since_cleanup() (
        trap 'echo cleaning up...' INT TERM HUP
        if [[ $tmpfile ]]; then
            rm -f "$tmpfile"
        fi
    )
    trap 'content_not_changed_since_cleanup; trap - RETURN' RETURN
    trap 'return 1' INT TERM HUP

    tmpfile=$(mktemp) || return
    touch --date "@$timestamp" "$tmpfile" || return
    local newer=
    newer=$(find "$dir" -mindepth 1 ${filter:+-name "$filter"} -newer "$tmpfile" -print -quit) \
        || return
    [[ ! $newer ]]
}

original_updated() {
    local original_file=$1 snapshot_file=$2

    [[ $original_file -nt $snapshot_file ]] \
        && ! diff --brief "$original_file" "$snapshot_file" &>/dev/null
}

################################################################'
# Polymorphic utilities for common operations on targets, toolings and the SDK itself.
#
# Notice the use of the terms 'object' and 'type'.

get_all_of_type() {
    local type=$1
    get_all_of_type_$type
}

get_all_or_selected_of_type() {
    local type=$1
    shift

    local objects= object= get_all=
    if [[ ${1:-} == "--all" ]]; then
        objects=($(get_all_of_type "$type")) || return
        get_all=1
        shift
    else
        objects=("$@")
        for object in "${objects[@]}"; do
            assert_object_exists "$type:$object" || return
        done
    fi

    # Exclude snapshots unless required explicitly
    if [[ $type == target ]]; then
        local objects_=("${objects[@]}")
        objects=()
        for object in "${objects_[@]}"; do
            ! target_is_snapshot "$object" || ! [[ $get_all ]] || continue
            objects+=("$object")
        done
    fi

    # Fail here if --all was not used and no objects were given
    [[ ${#objects[*]} -ne 0 || $get_all ]] || { usage >&2; return 1; }

    echo -n "${objects[@]}"
}

print_object() {
    local type=${1%:*} object=${1#*:}

    case $type in
        sdk) echo -n "SDK";;
        *) echo -n "$type '$object'";;
    esac
}

assert_object_exists() {
    local type=${1%:*} object=${1#*:}

    assert_${type}_exists "$object"
}

get_object_root() {
    local type=${1%:*} object=${1#*:}

    local root=
    case $type in
        sdk) root=/;;
        target) root=$MER_TARGETS/$object;;
        tooling) root=$MER_TOOLINGS/$object;;
    esac

    printf '%s\n' "$root"
}

enter() {
    local type=${1%:*} object=${1#*:} cmd=("${@:2}")

    if [[ $type == sdk ]]; then
        sudo "${cmd[@]}"
    else
        enter_$type "$object" "${cmd[@]}"
    fi
}

object_config_file()
{
    local type=${1%:*} object=${1#*:}
    ${type}_config_file "$object"
}

object_config_get()
{
    local object=$1 key=$2 default=${3:-}
    local config_file=$(object_config_file "$object")
    local value=
    if [[ -e "$config_file" ]]; then
        value=$(sed -n "s/^$key=//p" "$config_file") || return
    fi
    echo "${value:-$default}"
}

object_config_get_boolean()
{
    local value=
    value=$(object_config_get "$@") || return
    tr -d '0' <<<$value
}

object_config_set()
{
    local type=${1%:*} object=${1#*:} key=$2 value=$3
    local config_file=$(object_config_file "$type:$object")
    local maybe_sudo=
    if [[ $type != target ]]; then
        maybe_sudo=sudo
    fi
    # The following sed command needs at least one line
    $maybe_sudo tee -a "$config_file" <<<'' >/dev/null || return
    $maybe_sudo sed -i -e "1i $key=$value" -e "/^$key=/d" -e '/^$/d' "$config_file"
}

get_object_mode()
{
    local object=$1
    local mode=
    object_config_get "$object" "$MODE_KEY" "$MODE_USER"
}

set_object_mode()
{
    local object=$1 value=$2
    object_config_set "$object" "$MODE_KEY" "$value"
}

object_zypper() {
    local object=$1 args=("${@:2}")

    enter "$object" zypper ${NON_INTERACTIVE:+--non-interactive} "${args[@]}"
    local rc=$?

    (( rc < 100 || rc > 128 )) && return $rc

    case $rc in
        100) # ZYPPER_EXIT_INF_UPDATE_NEEDED
            return 0
            ;;
        101) # ZYPPER_EXIT_INF_SEC_UPDATE_NEEDED
            return 0
            ;;
        102) # ZYPPER_EXIT_INF_REBOOT_NEEDED
            return 0
            ;;
        103) # ZYPPER_EXIT_INF_RESTART_NEEDED
            object_zypper "$@"
            return
            ;;
        104) # ZYPPER_EXIT_INF_CAP_NOT_FOUND
            return $rc
            ;;
        105) # ZYPPER_EXIT_ON_SIGNAL
            return $rc
            ;;
        106) # ZYPPER_EXIT_INF_REPOS_SKIPPED
            return 0
            ;;
        107) # ZYPPER_EXIT_INF_RPM_SCRIPT_FAILED
            return 0
            ;;
    esac

    echo >&2 "WARNING: Unhandled zypper exit code $rc. Please report."
    return $rc
}

object_which() {
    local object=$1 tool=$2
    enter "$object" bash -c 'PATH=/usr/libexec/sdk-setup:$PATH which '"$tool"
}

register_object() {
    local type=$1
    shift

    local credentials= username= password= other=()
    credentials=$(get_register_credentials "$@") || return
    unpack "$credentials" username password other

    local force= i=
    for ((i=0; i < ${#other[@]}; ++i)); do
        if [[ ${other[i]} == --force ]]; then
            force=1
            unset other[i]
            break
        fi
    done

    set -- ${other[@]:+"${other[@]}"}

    local objects=
    if [[ $type == sdk ]]; then
        [[ $# -eq 0 ]] || { usage >&2; return 1; }
        objects=('')
    else
        objects=($(get_all_or_selected_of_type "$type" "$@")) || return
    fi

    local object= domain=
    for object in "${objects[@]}"; do
        object=$type:$object
        sdk_register=$(object_which "$object" sdk-register)
        domain=$(enter "$object" "$sdk_register" -d) || return
        if [[ $domain != jolla && ! $force ]]; then
            echo >&2 "Registration not needed for $(print_object "$object") (domain: ${domain:-empty})"
            continue
        fi

        echo >&2 -n "$(print_object "$object"): "
        enter "$object" "$sdk_register" -u "$username" -p "$password" || return
    done
}

register_all() {
    local credentials= username= password= other=()
    credentials=$(get_register_credentials "$@") || return
    unpack "$credentials" username password other

    set -- ${other[@]:+"${other[@]}"}

    local force= no_sdk=
    while [[ $# -gt 0 ]]; do
        case $1 in
            --force)
                force=1
                ;;
            --no-sdk)
                no_sdk=1
                ;;
            *)
                echo >&2 "unrecognized option '$1'"
                usage >&2
                return 1
                ;;
        esac
        shift
    done

    set -- --user "$username" --password "$password" ${force:+--force}

    [[ $no_sdk ]] || manage_sdk register "$@" || return
    manage_toolings register "$@" --all || return
    manage_targets register "$@" --all
}

refresh_objects() {
    local type=$1
    shift

    local objects=
    if [[ $type == sdk ]]; then
        objects=('')
    else
        objects=($(get_all_or_selected_of_type "$type" "$@")) || return
    fi

    local object=
    for object in "${objects[@]}"; do
        echo >&2 "Refreshing $(print_object "$type:$object")..."
        object_zypper "$type:$object" clean --raw-metadata
        object_zypper "$type:$object" refresh -f
        echo >&2
    done
}

object_upgradable() {
    local type=$1 object=${2:-}

    if [[ $type != sdk ]]; then
        assert_object_exists "$type:$object" || return
    fi

    object_zypper "$type:$object" --quiet --no-refresh list-updates
}

rebuild_rpmdb() {
    local object=$1

    # rpm can't overwrite directory on a different overlay layer
    if [[ $(findmnt --noheadings --output FSTYPE / ) == overlay ]]; then
        # Use rpmdb directly to work around an sb2 bug
        enter "$object" rpmdb --rebuilddb 2>&1 | sed -e '/failed to replace old database/,+1d'
        local new_db=$(enter "$object" find /var/lib -maxdepth 1 -name 'rpmrebuilddb.*' |head -n1)
        if [[ $new_db ]]; then
            # sb2 recreates rpm directory on startup, so we need to do these in
            # a single session
            enter "$object" sh -c "rm -rf /var/lib/rpm && mv $new_db /var/lib/rpm"
        fi
    else
        enter "$object" rpmdb --rebuilddb
    fi
}

upgrade_object() {
    local type=$1 object=${2:-}

    if [[ $type != sdk ]]; then
        assert_object_exists "$type:$object" || return
    fi

    # Rebuild RPM database prior to and after upgrading a target to prevent "Database environment
    # version mismatch" errors e.g. after upgrading RPM
    if [[ $type == target ]]; then
        # Ignore possible errors here in hope that they will disappear after the upgrade
        rebuild_rpmdb "$type:$object"
    fi

    object_zypper "$type:$object" dup || return

    if [[ $type == target ]]; then
        rebuild_rpmdb "$type:$object" || return

        if [[ ! ${SDK_MANAGE_NO_REINIT:-} ]]; then
            if ! target_uses_host_gcc "$object" && which gcc >/dev/null; then
                echo >&2 "Newly found host GCC - reinitializing target..."
                reinit_target "$object" || return
            fi
        fi
    fi

    enter "$type:$object" oneshot || return
}

init_machine_id() {
    local type=$1 object=$2

    if [[ $type == sdk ]]; then
        echo >&2 "init_machine_id: not intented to be used on sdk"
        return 1
    fi

    echo >&2 "Initializing machine ID for $type '$object'"

    if enter "$type:$object" test -s /etc/machine-id; then
        echo >&2 "NOTICE: removing unexpected non-empty /etc/machine-id in the '$object' $type"
        enter "$type:$object" rm -f /etc/machine-id || return
    fi
    if enter "$type:$object" test -s /var/lib/dbus/machine-id; then
        echo >&2 "NOTICE: removing unexpected non-empty /var/lib/dbus/machine-id in the '$object' $type"
        enter "$type:$object" rm -f /var/lib/dbus/machine-id || return
    fi

    enter "$type:$object" dbus-uuidgen --ensure || return
    # Do not use systemd-machine-id-setup for its unwanted side effects
    enter "$type:$object" cp /var/lib/dbus/machine-id /etc/machine-id || return
}

reset_machine_id() {
    local type=$1
    shift

    if [[ $type == sdk ]]; then
        echo >&2 "reset_machine_id: not intented to be used on sdk"
        return 1
    fi

    local objects=
    objects=($(get_all_or_selected_of_type "$type" "$@")) || return

    local object=
    for object in "${objects[@]}"; do
        echo >&2 "Resetting machine ID for $type '$object'"

        enter "$type:$object" rm -f /etc/machine-id || return
        enter "$type:$object" rm -f /var/lib/dbus/machine-id || return
        enter "$type:$object" dbus-uuidgen --ensure || return
        enter "$type:$object" cp /var/lib/dbus/machine-id /etc/machine-id || return
    done
}

maintain() {
    local type=$1
    shift

    local opt_no_sync=
    local object=
    local command=()

    while [[ $# -gt 0 ]]; do
        case $1 in
            --no-sync)
                opt_no_sync=1
                ;;
            -*)
                echo >&2 "unrecognized option '$1'"
                usage
                return 1
                ;;
            *)
                object=$1
                shift
                command=("$@")
                shift $#
                break
                ;;
        esac
        shift
    done

    if [[ ! $object ]]; then
        echo >&2 "$type name expected"
        usage
        return 1
    fi

    local session_start=
    session_start=$(date +%s)

    if [[ ${#command[@]} -gt 0 ]]; then
        enter "$type:$object" "${command[@]}"
    else
        enter "$type:$object" env PS1="[$object] \W # " /bin/bash --noprofile --norc
    fi

    local rc=$?
    [[ $rc -eq 0 ]] || return $rc

    if [[ ! $opt_no_sync ]]; then
        maybe_synchronise_object "$type:$object" "$session_start"
    fi

    return 0
}

################################################################'
# tooling management

tooling_exists() [[ -e $MER_TOOLINGS/$1 ]]

# specialization of object_config_file()
tooling_config_file()
{
    local tooling=$1
    echo "$MER_TOOLINGS/$tooling/$CONFIG_FILE"
}

# specialization of assert_object_exists()
assert_tooling_exists() {
    local tooling=$1

    assert_name_valid "$tooling" || return
    if ! tooling_exists "$tooling"; then
        echo >&2 "Unknown tooling: $tooling"
        return 1
    fi
}

# specialization of enter()
enter_tooling() {
    local tooling=$1 cmd=("${@:2}")

    assert_tooling_exists "$tooling" || return
    sudo "$MER_TOOLINGS/$tooling/mer-tooling-chroot" "${cmd[@]}"
}

# specialization of get_all_of_type()
get_all_of_type_tooling() {
    local tooling
    local toolings=$(find "$MER_TOOLINGS" -mindepth 1 -maxdepth 1 -type d -printf '%f\n')
    for tooling in $toolings; do
        if ! assert_name_valid "$tooling" 2>/dev/null; then
            echo >&2 "Ignoring tooling with invalid name '$tooling'"
            continue
        fi
        echo "$tooling"
    done
}

list_toolings() {
    local long=
    while [[ $# -gt 0 ]]; do
        case $1 in
            -l|--long)
                long=1
                ;;
            *)
                echo >&2 "unrecognized option '$1'"
                usage >&2
                return 1
                ;;
        esac
        shift
    done

    if [[ ! $long ]]; then
        get_all_of_type_tooling
        return
    fi

    local tooling=
    for tooling in $(get_all_of_type_tooling); do
        echo "$tooling" "$(get_object_mode "tooling:$tooling")"
    done |column -t
}

# Our toolchains are available through patterns-sailfish-sb2-* meta-packages
get_toolchains() {
    local tooling=$1
    object_zypper "tooling:$tooling" --no-refresh search "patterns-sailfish-sb2-*" | \
	grep patterns-sailfish-sb2 |grep -v sb2-common | while IFS='| ' read installed pkg dummy; do
	echo "${pkg},${installed}"
    done
    return ${PIPESTATUS[0]}
}

ensure_installed() {
    local tooling=$1 toolchain=$2
    # Do not require building zypper caches as this is not always possible
    enter_tooling "$tooling" rpm -qa --queryformat='%{name}\n' |grep -q -F --line-regexp "$toolchain"
}

ensure_installable() {
    local tooling=$1 toolchain=$2
    get_toolchains "$tooling" |grep -q -F --line-regexp "$toolchain,"
}

install_toolchain() {
    local tooling=$1 pkg=$2
    if ! ensure_installable "$tooling" "$pkg"; then
        echo >&2 "Toolchain '$pkg' doesn't exist or is already installed - can not install."
        return 1
    fi

    object_zypper "tooling:$tooling" --no-refresh install "$pkg"
}

install_tooling() {
    local name=$1 url=$2

    assert_name_valid "$name" || return

    if tooling_exists "$name"; then
        echo >&2 "Name already in use: $name"
        return 1
    fi

    sudo mkdir -p "$MER_TOOLINGS" || return

    local download_result= local_file= downloaded=

    local succeeded=
    install_tooling_cleanup() (
        trap 'echo cleaning up...' INT TERM HUP
        if [[ $download_result ]]; then
            local local_file= downloaded=
            if unpack "$download_result" local_file downloaded && [[ $downloaded ]]; then
                rm -f "$local_file"
            fi
        fi
        if [[ ! $succeeded ]]; then
            sudo rm -rf "$MER_TOOLINGS/$name"
        fi
    )
    trap 'install_tooling_cleanup; trap - RETURN' RETURN
    trap 'return 1' INT TERM HUP

    download_result=$(download "$url" "$TMPDIR_DOWNLOADS" "$name") || return
    unpack "$download_result" local_file downloaded

    sudo rm -rf "$MER_TOOLINGS/$name" || return
    sudo mkdir -p "$MER_TOOLINGS/$name" || return

    echo >&2 "Unpacking tooling ..."
    if ! maybe_decompress_tarball "$local_file" | sudo tar -C "$MER_TOOLINGS/$name" -x; then
        if disk_full "$MER_TOOLINGS"; then
            echo >&2 "Not enough disk space to unpack tooling image"
        else
            echo >&2 "Could not unpack tooling image"
        fi
        return 1
    fi

    # FIXME we have INSIDE_CHROOT and INSIDE_VBOX, but the latter is true also under docker
    if [[ $(systemd-detect-virt) == docker ]]; then
        sudo sed -i "$MER_TOOLINGS/$name/mer-tooling-chroot" -e '
            /^if cmp .*mountinfo/ {
                i \# sdk-manage disabled this under docker
                s/^/#/
                a \if false\; then
            }
            /^\s*mount .*resolv.conf/ {
                i \    # sdk-manage replaced this under docker
                h
                s/mount/#mount/
                p
                g
                s/mount -o bind,ro /cp /
            }'
    fi

    if [[ $MODE == "$MODE_USER" ]]; then
        if [[ $INSIDE_VBOX ]]; then
            local size=$(sudo du --summarize --bytes "$MER_TOOLINGS/$name" |cut -f1)
            manage_user_targets_repository touch tooling "$name" "${size:-0}"
        fi
    else
        set_object_mode "tooling:$name" "$MODE"
    fi

    init_machine_id tooling "$name" || return

    configure_http_proxy "tooling:$name"

    if [[ $INSIDE_VBOX ]]; then
        update_df_cache
    fi

    echo >&2 "Tooling '$name' set up"
    succeeded=1
}

clone_tooling() {
    local positional_args=()
    while [[ $# -gt 0 ]]; do
        case $1 in
            -*)
                echo >&2 "unrecognized option '$1'"
                usage >&2
                return 1
                ;;
            *)
                if [[ ${#positional_args[*]} -eq 2 ]]; then
                    echo >&2 "unexpected positional argument '$1'"
                    usage >&2
                    return 1
                fi
                positional_args+=("$1")
                ;;
        esac
        shift
    done

    local original=${positional_args[0]}
    local clone=${positional_args[1]}

    if ! tooling_exists "$original"; then
        echo >&2 "Tooling not found '$original'"
        return 1
    fi

    if tooling_exists "$clone"; then
        echo >&2 "Tooling of this name already exists '$clone'"
        return 1
    fi

    local succeeded=
    clone_tooling_cleanup() (
        trap 'echo cleaning up...' INT TERM HUP
        if [[ ! $succeeded ]]; then
            sudo rm -rf "$MER_TOOLINGS/$clone"
        fi
    )
    trap 'clone_tooling_cleanup; trap - RETURN' RETURN
    trap 'return 1' INT TERM HUP

    echo >&2 "Taking snapshot '$clone' of '$original' tooling..."
    sudo_sync_trees "$MER_TOOLINGS/$original" "$MER_TOOLINGS/$clone" || return

    reset_machine_id tooling "$clone" || return

    if [[ $INSIDE_VBOX ]]; then
        update_df_cache
    fi

    set_object_mode "tooling:$clone" "$MODE_USER" || return
    if [[ $INSIDE_VBOX ]]; then
        local size=$(sudo du --summarize --bytes "$MER_TOOLINGS/$clone" |cut -f1)
        manage_user_targets_repository touch tooling "$clone" "${size:-0}"
    fi

    echo >&2 "Tooling '$clone' set up"
    succeeded=1
}

remove_tooling() {
    local name=$1

    assert_name_valid "$name" || return

    local mode=$(get_object_mode "tooling:$name")
    if [[ $MODE != "$mode" ]]; then
        echo >&2 "The tooling '$name' can only be removed in the '$mode' mode."
        return 1
    fi

    local targets=$(get_targets_using_tooling "$name")
    if [[ $targets ]]; then
        targets=$(echo $targets |sed 's/ /, /')
        echo >&2 "Cannot remove tooling '$name': Tooling used by following targets: '$targets'"
        return 1
    fi

    sudo rm -rf "$MER_TOOLINGS/$name"

    if [[ $INSIDE_VBOX && $mode == "$MODE_USER" ]]; then
        manage_user_targets_repository rm tooling "$name"
    fi

    if [[ $INSIDE_VBOX ]]; then
        update_df_cache
    fi
}

manage_toolings() {
    if ! [[ ${1:-} ]]; then
        usage >&2
        return 1
    fi

    case $1 in
        ?(--)list ) shift
            list_toolings "$@"
            ;;
        ?(--)upgradable ) shift
            object_upgradable tooling "$@"
            ;;
        ?(--)install ) shift
            install_tooling "$@"
            ;;
        ?(--)clone ) shift
            clone_tooling "$@"
            ;;
        ?(--)remove ) shift
            remove_tooling "$@"
            ;;
        ?(--)refresh ) shift
            refresh_objects tooling "$@"
            ;;
        ?(--)update ) shift
            upgrade_object tooling "$@"
            ;;
        ?(--)register ) shift
            register_object tooling "$@"
            ;;
        ?(--)package-list ) shift
            list_object_packages tooling "$@"
            ;;
        ?(--)package-install ) shift
            manage_object_packages install tooling "$@"
            ;;
        ?(--)package-remove ) shift
            manage_object_packages remove tooling "$@"
            ;;
        ?(--)package-diff ) shift
            diff_object_packages tooling "$@"
            ;;
        ?(--)package-cache-prune ) shift
            prune_object_package_cache tooling "$@"
            ;;
        ?(--)uuidgen ) shift
            reset_machine_id tooling "$@"
            ;;
        ?(--)maintain ) shift
            maintain tooling "$@"
            ;;
        * )
            echo >&2 "unrecognized option '$1'"
            usage >&2
            return 1
            ;;
    esac
}

################################################################'
# Toolchains

manage_toolchains() {
    if ! [[ {$1:-} ]]; then
        usage >&2
        return 1
    fi

    case $1 in
        ?(--)list ) shift
            local tooling=${1:-}
            assert_tooling_exists "$tooling" || return
            get_toolchains "$tooling"
            ;;
        ?(--)install ) shift
            local tooling=${1:-}
            assert_tooling_exists "$tooling" || return
            shift
            object_zypper "tooling:$tooling" --no-refresh install "$@" || return
            ;;
        ?(--)remove ) shift
            local tooling=${1:-}
            assert_tooling_exists "$tooling" || return
            shift
            object_zypper "tooling:$tooling" --no-refresh remove "$@" || return
            ;;
        * )
            echo >&2 "unrecognized option '$1'"
            usage >&2
            return 1
            ;;
    esac
}

################################################################'
# Devel packages

# Our devel packages are available through zypper as -devel packages

# specialization of assert_object_exists()
assert_target_exists() {
    local target=$1
    assert_name_valid "$target" || return
    if ! [[ -f $SBOX2DIR/$target/sb2.config ]]; then
        echo >&2 "Target '$target' is not a valid Scratchbox2 target"
        return 1
    fi

    if ! check_target_visible "$target"; then
        echo >&2 "Target '$target' is not accessible"
        return 1
    fi
}

# specialization of enter()
enter_target() {
    local target=$1
    shift
    sb2 -t "$target" -m sdk-install -R "$@"
}

list_object_packages() {
    local type=$1

    shift
    local long=
    local positional_args=()
    while [[ $# -gt 0 ]]; do
        case $1 in
            -l|--long)
                long=1
                ;;
            --)
                shift
                positional_args+=("$@")
                break
                ;;
            -*)
                echo >&2 "unrecognized option '$1'"
                usage >&2
                return 1
                ;;
            *)
                positional_args+=("$1")
                ;;
        esac
        shift
    done

    set -- ${positional_args[@]:+"${positional_args[@]}"}

    if [[ $# -lt 1 ]]; then
        echo >&2 "$type name expected"
        return 1
    fi

    local object=$1 search=("${@:2}")

    assert_object_exists "$type:$object" || return

    local separator= format=
    if [[ $long ]]; then
        separator='\\| '
        format='{
            status = $1 ~ "i" ? "installed" : "available";
            name = $2; // keep the padding
            description = $3; sub(/ *$/, "", description); // strip the padding
            print name, status "  " description
        }'
    else
        separator=' *\\| *'
        format='{print $2 "," $1}'
    fi

    # do not include sourcepackages to the list (-t package -t pattern)
    object_zypper "$type:$object" --no-refresh search -t package -t pattern \
            -- ${search[@]:+"${search[@]}"} 2>&1 \
        |sed '0,/---------/d' \
        |awk -F "$separator" "$format"
    return ${PIPESTATUS[0]}
}

manage_object_packages() {
    local action=$1 type=$2 object=${3:-} packages=("${@:4}")

    assert_object_exists "$type:$object" || return

    if [[ ${#packages[@]} -eq 0 ]]; then
        echo >&2 "no package name given"
        return 1
    fi

    object_zypper "$type:$object" --no-refresh "$action" "${packages[@]}" || return
    maybe_synchronise_object "$type:$object"
}

diff_object_packages() {
    local type=$1

    shift
    local positional_args=()
    while [[ $# -gt 0 ]]; do
        case $1 in
            -*)
                echo >&2 "unrecognized option '$1'"
                usage >&2
                return 1
                ;;
            *)
                positional_args+=("$1")
                ;;
        esac
        shift
    done

    set -- ${positional_args[@]:+"${positional_args[@]}"}

    if [[ $# -ne 2 ]]; then
        echo >&2 "Exactly two $type names expected"
        return 1
    fi

    local now_object=$1 old_object=$2

    assert_object_exists "$type:$now_object" || return
    assert_object_exists "$type:$old_object" || return

    local query_format='%{NAME} %{EVR} %{VENDOR}\n'

    local now_packages= old_packages=
    now_packages=$(enter "$type:$now_object" rpm -q --queryformat "$query_format" -a) || return
    old_packages=$(enter "$type:$old_object" rpm -q --queryformat "$query_format" -a) || return

    diff_package_lists "$old_packages" "$now_packages"
}

diff_package_lists() {
    local old_packages=$1 now_packages=$2

    old_packages=$(sort -k1,1 -k2,2V -k3,3 <<<$old_packages) || return
    now_packages=$(sort -k1,1 -k2,2V -k3,3 <<<$now_packages) || return

    local added_packages=$(join -v 1 <(cat <<<"$now_packages") <(cat <<<"$old_packages"))
    local removed_packages=$(join -v 2 <(cat <<<"$now_packages") <(cat <<<"$old_packages"))
    local same_packages=$(grep -F --line-regexp -f <(cat <<<"$old_packages") <<<$now_packages)
    local replacement_packages=$(grep -F --line-regexp -v \
        -f <(cat <<<"$added_packages"$'\n'"$same_packages") <<<$now_packages)
    local replaced_packages=$(join <(cat <<<"$old_packages") <(cat <<<"$replacement_packages"))

    added_packages=$(awk -v OFS=, '($1) { print $1, $2 }' <<<$added_packages)
    removed_packages=$(awk -v OFS=, '($1) { print $1, $2 }' <<<$removed_packages)
    replaced_packages=$(awk -v OFS=, '
        ($1) {
            print $1,
                ($2 == $4 ? $2 : ($2 " -> " $4)),
                ($3 == $5 ? "" : "(vendor changed)")
        }' <<<$replaced_packages)

    {
        echo "Added:"
        echo ,
        printf '%s\n' "${added_packages:-(none)}"

        echo ,
        echo "Removed:"
        echo ,
        printf '%s\n' "${removed_packages:-(none)}"

        echo ,
        echo "Replaced:"
        echo ,
        printf '%s\n' "${replaced_packages:-(none)}"
    } |column -t --separator , |sed 's/ \+$//'
}

prune_object_package_cache() {
    local type=$1 object=${2:-}

    assert_object_exists "$type:$object" || return

    # Snapshots are configured to use original target's package cache
    if [[ $type == target ]] && target_is_snapshot "$object"; then
        object=$(object_config_get "target:$object" "$SNAPSHOT_OF_KEY") || return
    fi

    local root=$(get_object_root "$type:$object")
    local cache_root=$root/var/cache/zypp/packages

    [[ -d $cache_root ]] || return 0

    local queryformat='%{NAME},%{EPOCH},%{VERSION},%{RELEASE},{}\n'
    local dir= dirs=$(find "$cache_root" -type f -name '*.rpm' -printf '%h\n' |sort -u)
    for dir in $dirs; do
        find "$dir" -maxdepth 1 -type f -name "*.rpm" \
            |xargs -I{} rpm -q --nosignature --queryformat "$queryformat" -p {} \
            `# sort by name and e-v-r, latest first` \
            |sort -t ',' -k 1,1 -k2,2rV -k3,3rV -k4,4rV \
            `# drop latest from the list, print file paths` \
            |awk -F ',' 'BEGIN { prev="" }; ($1 == prev) { print $5}; { prev=$1 }' \
            |xargs --no-run-if-empty rm -f
    done
}

maybe_synchronise_object() {
    local type=${1%:*} object=${1#*:} timestamp=${2:-}

    [[ $INSIDE_VBOX ]] || return 0

    # FIXME Currently we also synchronize bits from toolings, so ideally we
    # should synchronize all targets when a tooling is changed
    [[ $type == target ]] || return 0

    local sync=
    sync=$(object_config_get_boolean "target:$object" "$SYNC_TO_HOST_KEY") || return
    [[ $sync ]] || return 0

    if [[ $timestamp ]]; then
        rpm_db_not_changed_since "$MER_TARGETS/$object/var/lib/rpm" "$timestamp" && return 0
    fi

    local original=
    original=$(object_config_get "target:$object" "$SNAPSHOT_OF_KEY") || return
    if [[ $original && $object != "$original".* ]]; then
        # mb2 works with snapshots named after the original targets by adding a suffix
        notice "Internal error: Cannot synchronize arbitrary named targets"
        return 1
    fi

    synchronise_target "$object" || return
    updateQtCreatorTargets --name "$object" --origin "$original" \
        --target-xml "$TARGETS_XML" || return
}

manage_develpkgs() {
    if ! [[ {$1:-} ]]; then
        usage >&2
        return 1
    fi

    case $1 in
	?(--)list ) shift
            list_object_packages target "$@"
	    ;;
	?(--)install ) shift
            manage_object_packages install target "$@" || return
	    ;;
	?(--)remove ) shift
            manage_object_packages remove target "$@" || return
	    ;;
	* )
            echo >&2 "unrecognized option '$1'"
            usage >&2
            return 1
	    ;;
    esac
}

################################################################
# Targets

# specialization of object_config_file()
target_config_file()
{
    local target=$1
    echo "$MER_TARGETS/$target/$CONFIG_FILE"
}

target_exists() {
    local target=$1
    # sb2-config is verbose when no target exists
    sb2-config -f 2>/dev/null |grep -q -F --line-regexp "$target"
}

target_is_snapshot() {
    local target=$1
    [[ $(object_config_get "target:$target" "$SNAPSHOT_OF_KEY") ]]
}

check_target_visible() (
    local target=$1
    set +o nounset
    . $SBOX2DIR/$target/sb2.config
    test -d "$SBOX_TARGET_ROOT"
)

target_uses_host_gcc() (
    local target=$1
    set +o nounset
    . "$SBOX2DIR/$target/sb2.config" || return
    [[ $SBOX_HOST_GCC_NAME ]]
)

# Specialization of get_all_of_type()
get_all_of_type_target() {
    # sb2-config is verbose when no target exists
    sb2-config -f 2>/dev/null
}

get_tooling_used_by_target() (
    local target=$1
    set +o nounset
    . $SBOX2DIR/$target/sb2.config
    if [[ $SBOX_TOOLS_ROOT == "$MER_TOOLINGS"/* ]]; then
        echo "${SBOX_TOOLS_ROOT#$MER_TOOLINGS/}"
    else
        return 1
    fi
)

get_targets_using_tooling() {
    local tooling=$1
    local target=
    for target in $(get_all_of_type_target); do
        [[ $(get_tooling_used_by_target "$target") == $tooling ]] && echo "$target"
    done
}

get_snapshots_of_target() {
    local target=$1
    local other=
    for other in $(get_all_of_type_target); do
        local snapshot_of=
        snapshot_of=$(object_config_get "target:$other" "$SNAPSHOT_OF_KEY")
        if [[ $snapshot_of == "$target" ]]; then
            echo "$other"
        fi
    done
}

list_targets() {
    local long= tooling= no_snapshots= check_snapshots= snapshots_of=
    while [[ $# -gt 0 ]]; do
        case $1 in
            -l|--long)
                long=1
                ;;
            --check-snapshots)
                check_snapshots=1
                ;;
            --no-snapshots)
                no_snapshots=1
                ;;
            --snapshots-of)
                snapshots_of=${2:-}
                if [[ ! $snapshots_of ]]; then
                    echo >&2 "$1: Argument expected"
                    usage >&2
                    return 1
                fi
                shift
                ;;
            --tooling)
                tooling=${2:-}
                if [[ ! $tooling ]]; then
                    echo >&2 "$1: Argument expected"
                    usage >&2
                    return 1
                fi
                shift
                ;;
            *)
                echo >&2 "unrecognized option '$1'"
                usage >&2
                return 1
                ;;
        esac
        shift
    done

    if [[ $snapshots_of && $tooling ]]; then
        echo >&2 "Cannot combine '--snapshots-of' with '--tooling'"
        usage >&2
        return 1
    fi

    if [[ $snapshots_of && $no_snapshots ]]; then
        echo >&2 "Cannot combine '--snapshots-of' with '--no-snapshots'"
        usage >&2
        return 1
    fi

    if [[ $check_snapshots && ! $long ]]; then
        echo >&2 "Cannot use '--check-snapshots' without '--long'"
        usage >&2
        return 1
    fi

    local targets=
    if [[ $tooling ]]; then
        targets=$(get_targets_using_tooling "$tooling") || return
    elif [[ $snapshots_of ]]; then
        targets=$(get_snapshots_of_target "$snapshots_of") || return
    else
        targets=$(get_all_of_type_target) || return
    fi

    local target= mode= snapshot_of= needs_reset=
    for target in $targets; do
        snapshot_of=$(object_config_get "target:$target" "$SNAPSHOT_OF_KEY")
        if [[ $no_snapshots && $snapshot_of ]]; then
            continue
        fi

        if [[ ! $long ]]; then
            echo "$target"
            continue
        fi

        tooling=$(get_tooling_used_by_target "$target") # may fail
        mode=$(get_object_mode "target:$target")
        needs_reset=
        if [[ $snapshot_of && $check_snapshots ]]; then
            if target_snapshot_needs_reset "$target" "$snapshot_of"; then
                needs_reset=*
            fi
        fi
        echo "$target" "${tooling:--}" "$mode" "${snapshot_of:--}${needs_reset}"
    done |column -t
    return ${PIPESTATUS[0]}
}

synchronise_target() {
    local target=$1
    # This is now a minimal set of files to sync
    if ! [[ -d $MER_TARGETS/$target ]]; then
        echo >&2 "No target called '$target'"
        return 1
    fi

    local libdir=/usr/lib
    if [[ -d $MER_TARGETS/$target/usr/lib64 ]]; then
        libdir=/usr/lib64
    fi

    # See GccToolChain::gccHeaderPaths() in Qt Creator sources.
    local system_includes=$(
        sb2 -t "$target" gcc -x c++ -E -v /dev/null 2>&1 |awk '
            BEGIN { inside_search_list = 0 }
            /^#include/ { inside_search_list = 1; next }
            (!inside_search_list) { next }
            /^End of search list./ { exit }
            { print }
        '
    )
    # These are not contained in the target but it the tooling (or directly in
    # the SDK if tooling is not used with the target).
    local cross_includes=$(
        if [[ $system_includes ]]; then
            while read path; do
                path=$(readlink -f "$path")
                if [[ $path != "$MER_TARGETS/"* && $path == */opt/cross/* ]]; then
                    printf '%s\n' "$path"
                fi
            done <<<"$system_includes"
        fi
    )

    local maybe_times=--times
    if [[ $(systemd-detect-virt) == docker ]]; then
	# It would fail with EPERM
        maybe_times=
    fi

    generate_qml_bundle "$MER_TARGETS/${target}${libdir}"

    echo >&2 "Synchronising target to host..."
    mkdir -p "$HOST_TARGETS/$target" || return
    # The filter=". -" means to read filters from stdin (the <<EOF)
    rsync -r $maybe_times --no-devices --no-specials \
          --delete --ignore-errors \
	  --prune-empty-dirs  --copy-links \
	  --filter=". -" \
          "$MER_TARGETS/$target/." "$HOST_TARGETS/$target/." <<EOF || return
# It would be verbose about these, which we add manually to the destination
- /usr/bin
- /usr/bin/**
# It would be verbose about these, which we rsync from elsewhere
- /opt
- /opt/**
# Ignore broken symlinks - rsync would fail on them. This includes absolute
# symlinks - those would be unresolvable on the host anyway. ('--safe-links'
# would not help here. It would require '--archive' but we can't use that with
# vboxsf).
$(cd "$MER_TARGETS/$target/" && find -xtype l |sed 's,^\.,- ,')
# Ensure all dirs are copied (--prune-empty-dirs will clean up)
+ */
# We want this for QtCreator to determine the architecture
+ $libdir/libQt*Core.so
# Don't need any other .so files
- *.so
# All the import, include and qt* shares
+ $libdir/qt*/imports**
# Qt5 qml imports are here
+ $libdir/qt5/qml**
+ /usr/include**
+ $libdir/*/include**
+ /usr/share/qt**
+ $libdir/pkgconfig**
+ /usr/share/pkgconfig**
+ $libdir/cmake**
+ /usr/share/cmake/Modules**
+ /usr/share/cmake/include**
# and nothing else
- *
EOF

    if [[ $cross_includes ]]; then
        local src_path= dst_path=
        while read src_path; do
            dst_path=$HOST_TARGETS/$target/opt/cross/${src_path#*/opt/cross/}
            mkdir -p "$dst_path" || return
            rsync -r $maybe_times --delete --ignore-errors --prune-empty-dirs --copy-links \
                "$src_path/." "$dst_path/." || return
        done <<<"$cross_includes"
    fi

    # We need /usr/bin/stopCreatorSegfaulting for Creator's happiness
    mkdir -p "$HOST_TARGETS/$target/usr/bin" || return
    touch "$HOST_TARGETS/$target/usr/bin/stopCreatorSegfaulting" || return
    # For Qt5, QtCreator needs to see this dir or it thinks Qt version is not properly installed
    mkdir -p "$HOST_TARGETS/$target$libdir/qt5/bin/" || return
    # Qt Creator does not like GCC reporting include paths that do not exist
    mkdir -p "$HOST_TARGETS/$target/usr/local/include" || return
    # Qt Creator needs to know where to search for Linguist and Designer binaries
    cat <<EOF > "$HOST_TARGETS/$target/usr/share/qt5/mkspecs/modules/qt_lib_sailfishsdk.pri" || return
QT.designer.bins = \$\$absolute_path(../../../../../../../../bin/)
EOF
    # Qt Creator needs to distinguish our Qt
    sed -i "$HOST_TARGETS/$target/usr/share/qt5/mkspecs/common/linux.conf" \
        -e '/QMAKE_PLATFORM/s/$/ sailfishos/' || return

    echo >&2 "Sync completed"
}

import_target() {
    local target=$1
    # This could be minimised in the future if sf is not fixed
    # Ignore errors as we sometimes have dangling symlinks and
    # still want to keep clean
    if ! [[ -d $HOST_TARGETS/$target ]]; then
        echo >&2 "No host target called '$target'"
        return 1
    fi
    rsync -a --no-devices --no-specials \
          --delete --ignore-errors \
          "$HOST_TARGETS/$target/" "$MER_TARGETS/$target"
}

# add the MerSDK hostname to target's /etc/hosts
amend_target_hosts() {
    local mytarget=$1
    local myhostname=$(hostname)

    if [[ -z $mytarget ]]; then
	echo >&2 "NOTICE: amend_target_hosts: empty target name given"
	return 0
    fi

    enter_target "$mytarget" sed -i "s,\(127.0.0.1.*\),\1 $myhostname," /etc/hosts
}

amend_target_rpm_macros() {
    local target=$1

    # Older targets define __cmake with absolute path. mb2 needs it relative to
    # be able to wrap it.
    enter_target "$target" tee /etc/rpm/macros.cmake.sdk >/dev/null <<EOF
# Created by sdk-manage
%__cmake cmake
EOF
}

#generates sailfishos-bundle.json for QtCreator
generate_qml_bundle() {
    local libdir=$1
    local imports=$(sed -n -e 's,^\s*exports: \["\([^/]\+\)/[^ ]\+ \([0-9.]\+\)"\],"\1 \2",p' \
            "$libdir"/qt5/qml/**/plugins.qmltypes \
            |sort -u |sed '$! s/$/,/')

    local bundlefile=$libdir/qt5/qml/sailfishos-bundle.json
    cat - > "$bundlefile" <<EOF
{
    "name": "QtQuick2",
    "searchPaths": [
        "\$(QT_INSTALL_QML)"],
    "installPaths": [
        "\$(QT_INSTALL_QML)"],
    "implicitImports": [
        "__javascriptQt5__"],
    "supportedImports": [
        $(sed '2,$s/^/        /' <<<$imports)
    ]
}
EOF
}

tooling_matches_target()
{
    if [[ $1 == --quiet ]]; then
        exec 3>/dev/null
        shift
    fi

    local target_release=$1
    local tooling=$2

    local tooling_release=
    tooling_release=$(get_object_ssu_release "tooling:$tooling") || return
    if [[ $tooling_release != $target_release ]]; then
        echo >&3 "WARNING: Tooling release '$tooling_release' does not match target release '$target_release'"
        return 1
    fi
} 3>&2

find_suitable_tooling_or()
{
    local target_release=$1
    local tooling=${2:-}

    if [[ $tooling ]]; then
        tooling_matches_target "$target_release" "$tooling" || :
    else
        local available_tooling=
        for available_tooling in $(get_all_of_type_tooling); do
            if tooling_matches_target --quiet "$target_release" "$available_tooling"; then
                tooling=$available_tooling
                break
            fi
        done

        if [[ ! $tooling ]]; then
            echo >&2 "No suitable tooling found for this target"
            return 1
        fi

        echo >&2 "Using '$tooling' tooling for this target"
    fi

    printf '%s\n' "$tooling"
}

unpack_target() {
    local target=$1
    local tarball=$2

    echo >&2
    echo >&2 "Unpacking target ..."
    if ! maybe_decompress_tarball "$tarball" | sudo tar -C "$MER_TARGETS/$target" -x; then
        if disk_full "$MER_TARGETS"; then
            echo >&2 "Not enough disk space to unpack target image"
        else
            echo >&2 "Could not unpack target image"
        fi
        return 1
    fi
}

# Download and install a rootfs
install_target() {
    local skip_toolchain_check= tooling= tooling_url= toolchain= positional_args=()
    local no_snapshot=
    while [[ $# -gt 0 ]]; do
        case $1 in
            --jfdi)
	        # Sometimes you want to install without checking the toolchain - jfdi
                skip_toolchain_check=1
                ;;
            --no-snapshot)
                no_snapshot=1
                ;;
            --tooling)
                tooling=${2:-}
                if [[ ! $tooling ]]; then
                    echo >&2 "$1: Argument expected"
                    usage >&2
                    return 1
                fi
                shift
                ;;
            --tooling-url)
                tooling_url=${2:-}
                if [[ ! $tooling_url ]]; then
                    echo >&2 "$1: Argument expected"
                    usage >&2
                    return 1
                fi
                shift
                ;;
            --toolchain)
                toolchain=${2:-}
                if [[ ! $toolchain ]]; then
                    echo >&2 "$1: Argument expected"
                    usage >&2
                    return 1
                fi
                shift
                ;;
            -*)
                echo >&2 "unrecognized option '$1'"
                usage >&2
                return 1
                ;;
            *)
                if [[ ${#positional_args[*]} -eq 2 ]]; then
                    echo >&2 "unexpected positional argument '$1'"
                    usage >&2
                    return 1
                fi
                positional_args=(${positional_args[@]:+"${positional_args[@]}"} "$1")
                ;;
        esac
        shift
    done

    if [[ $tooling_url && ! $tooling ]]; then
        echo >&2 "'--tooling' required when '--tooling-url' is used"
        usage >&2
        return 1
    fi

    if [[ $tooling && ! $tooling_url ]]; then
        assert_tooling_exists "$tooling" || return
    fi

    if [[ ${#positional_args[*]} -ne 2 ]]; then
        echo >&2 "<name> and <URL> expected"
        usage >&2
        return 1
    fi

    local target=${positional_args[0]}
    local url=${positional_args[1]}

    assert_name_valid "$target" || return

    if target_exists "$target"; then
        echo >&2 "Target already exists '$target'"
        return 1
    fi

    # make sure the target dir exists
    if [[ ! -d $MER_TARGETS ]]; then
        sudo mkdir -p "$(dirname "$MER_TARGETS")" || return
        sudo install -d -o $(id -u) -g $(id -g) "$MER_TARGETS" || return
    fi

    local download_result= local_file= downloaded= tooling_installed=

    local succeeded=
    install_target_cleanup() (
        trap 'echo cleaning up...' INT TERM HUP
        if [[ $download_result ]]; then
            local local_file= downloaded=
            if unpack "$download_result" local_file downloaded && [[ $downloaded ]]; then
                rm -f "$local_file"
            fi
        fi
        if [[ ! $succeeded ]]; then
            rm -rf "$SBOX2DIR/$target"{,."$DEFAULT_SNAPSHOT_SUFFIX"}
            sudo rm -rf "$MER_TARGETS/$target"{,."$DEFAULT_SNAPSHOT_SUFFIX"}
            if [[ $INSIDE_VBOX ]]; then
                rm -rf "$HOST_TARGETS/$target"{,."$DEFAULT_SNAPSHOT_SUFFIX"}
            fi
            if [[ $tooling_installed ]]; then
                remove_tooling "$tooling"
            fi
        fi
    )
    trap 'install_target_cleanup; trap - RETURN' RETURN
    trap 'return 1' INT TERM HUP

    if [[ $tooling_url ]]; then
        if tooling_exists "$tooling"; then
            echo >&2 "Tooling '$tooling' already installed, skipping"
        else
            install_tooling "$tooling" "$tooling_url" || return
            tooling_installed=1
        fi
    fi

    download_result=$(download "$url" "$TMPDIR_DOWNLOADS" "$target") || return
    unpack "$download_result" local_file downloaded

    # Virtualbox shared folders don't work too well at the moment
    # Unpack targets to a private area within the VM
    sudo rm -rf "$MER_TARGETS/$target" || return
    sudo mkdir -p "$MER_TARGETS/$target" || return

    local target_unpacked=
    local target_release=

    local meta=$(maybe_get_tarball_meta_data "$local_file")
    if [[ $meta ]]; then
        target_release=$(get_object_ssu_release_from_tarball_meta_data "$meta")
    fi

    if [[ ! $target_release ]]; then
        unpack_target "$target" "$local_file" || return
        target_unpacked=1
        target_release=$(get_object_ssu_release "target:$target") || return
    fi

    tooling=$(find_suitable_tooling_or "$target_release" "$tooling") || return

    [[ $target_unpacked ]] || unpack_target "$target" "$local_file" || return

    local target_arch=
    target_arch=$(get_object_ssu_config "target:$target" arch) || return
    if ! [[ $toolchain ]]; then
        toolchain="patterns-sailfish-sb2-$target_arch"
    fi

    if ! [[ $skip_toolchain_check ]]; then
        echo >&2 "Making sure the right toolchain exists in '$tooling' tooling"
        if ! ensure_installed "$tooling" "$toolchain"; then
            echo >&2 "Installing required toolchain in '$tooling' tooling: $toolchain"
            install_toolchain "$tooling" "$toolchain" || return
        fi
    fi

    sudo chown -R $(id -u):$(id -g) "$MER_TARGETS/$target" || return

    echo >&2 "Setting up SB2"

    local transparency= compiler=
    case $target_arch in
        aarch64)
            transparency="/usr/bin/qemu-aarch64-dynamic"
            compiler="/opt/cross/bin/aarch64-meego-linux-gnu-gcc"
	    ;;
        arm*)
            transparency="/usr/bin/qemu-arm-dynamic"
            compiler="/opt/cross/bin/${target_arch}-meego-linux-gnueabi-gcc"
	    ;;
        mipsel)
            transparency="/usr/bin/qemu-mipsel-dynamic"
	    compiler="/opt/cross/bin/mipsel-meego-linux-gnu-gcc"
	    ;;
        i486*)
	    compiler="/opt/cross/bin/i486-meego-linux-gnu-gcc"
	    ;;
    esac

    if [[ $INSIDE_CHROOT ]]; then
        getent passwd $(id -u) |fix_shell |sudo tee -a "$MER_TARGETS/$target/etc/passwd" >/dev/null
        getent group $(id -u) |sudo tee -a "$MER_TARGETS/$target/etc/group" >/dev/null
    fi

    (
        cd "$MER_TARGETS/$target" && sb2-init \
            -L "--sysroot=/" -C "--sysroot=/" ${transparency:+-c "$transparency"} \
            -m sdk-build -n -N -t "$MER_TOOLINGS/$tooling" "$target" "$MER_TOOLINGS/${tooling}${compiler}"
    ) || return

    init_machine_id target "$target" || return

    configure_http_proxy "target:$target"

    # fix target's /etc/hosts
    amend_target_hosts "$target" || return

    amend_target_rpm_macros "$target" || return

    assert_target_exists "$target" || return

    rebuild_rpmdb "target:$target" || return

    if [[ $MODE == "$MODE_USER" ]]; then
        if [[ $INSIDE_VBOX ]]; then
            local size=$(sudo du --summarize --bytes "$MER_TARGETS/$target" |cut -f1)
            manage_user_targets_repository touch target "$target" "${size:-0}"
        fi
    else
        set_object_mode "target:$target" "$MODE"
    fi

    if [[ $no_snapshot ]]; then
        object_config_set "target:$target" "$SYNC_TO_HOST_KEY" 1 || return
        maybe_synchronise_object "target:$target" || return
    else
        object_config_set "target:$target" "$SYNC_TO_HOST_KEY" 0 || return
        echo >&2 "Creating default snapshot"
        snapshot_target "$target"{,."$DEFAULT_SNAPSHOT_SUFFIX"} || return
    fi

    if [[ $INSIDE_VBOX ]]; then
        update_df_cache
    fi

    echo >&2 "Target '$target' set up"
    succeeded=1
}

reinit_target() (
    local target=$1
    set +o nounset
    . "$SBOX2DIR/$target/sb2.config" || return
    cd "$SBOX_TARGET_ROOT" || return
    eval sb2-init $SBOX_INIT_ORIG_ARGS
)

snapshot_target() {
    local reset= sync=1 detach= positional_args=()
    while [[ $# -gt 0 ]]; do
        case $1 in
            -r|--reset)
                reset=outdated
                ;;
            -r=*|--reset=*)
                reset=${1#*=}
                if [[ $reset != @(soft|outdated|force) ]]; then
                    echo >&2 "unexpected argument to --reset: '$reset'"
                    usage >&2
                    return 1
                fi
                ;;
            --no-sync)
                sync=
                ;;
            --detach)
                detach=1
                ;;
            -*)
                echo >&2 "unrecognized option '$1'"
                usage >&2
                return 1
                ;;
            *)
                if [[ ${#positional_args[*]} -eq 2 ]]; then
                    echo >&2 "unexpected positional argument '$1'"
                    usage >&2
                    return 1
                fi
                positional_args+=("$1")
                ;;
        esac
        shift
    done

    if [[ $reset && $detach ]]; then
        echo >&2 "Cannot combine '--reset' and '--detach'"
        return 1
    fi

    local original=${positional_args[0]}
    local snapshot=${positional_args[1]}

    assert_name_valid "$original" || return
    assert_name_valid "$snapshot" || return

    if ! target_exists "$original"; then
        echo >&2 "Target not found '$original'"
        return 1
    fi

    local existed_before=
    if target_exists "$snapshot"; then
        existed_before=1
        if [[ ! $reset ]]; then
            echo >&2 "Target already exists '$snapshot'. Wanted to use '--reset'?"
            return 1
        fi
    fi

    local succeeded= config_backup= reset_started=
    snapshot_target_cleanup() (
        trap 'echo cleaning up...' INT TERM HUP
        if [[ ! $succeeded ]]; then
            if [[ $existed_before ]]; then
                if [[ $config_backup ]]; then
                    local config=$(object_config_file "target:$snapshot")
                    cat >"$config" <<<"$config_backup"
                fi
                if [[ $reset_started ]]; then
                    echo >&2 "Leaving the '$snapshot' target in a possibly inconsistent state"
                fi
            else
                rm -rf "$SBOX2DIR/$snapshot"
                sudo rm -rf "$MER_TARGETS/$snapshot"
                if [[ $INSIDE_VBOX ]]; then
                    rm -rf "$HOST_TARGETS/$snapshot"
                fi
            fi
        fi
    )
    trap 'snapshot_target_cleanup; trap - RETURN' RETURN
    trap 'return 1' INT TERM HUP

    if [[ ! $existed_before ]]; then
        local orig_sb2_config=~/.scratchbox2/$original/sb2.config
        local sb2_init_args=
        sb2_init_args=$(set +o nounset; \
            . "$orig_sb2_config" && printf '%s\n' "$SBOX_INIT_ORIG_ARGS") || return
        sb2_init_args=${sb2_init_args/ $original / $snapshot }

        echo >&2 "Taking snapshot '$snapshot' of '$original' target..."
        sudo_sync_trees "$MER_TARGETS/$original" "$MER_TARGETS/$snapshot" || return
        [[ $detach ]] || object_config_set "target:$snapshot" "$SNAPSHOT_OF_KEY" "$original" \
            || return
        ( cd "$MER_TARGETS/$snapshot" && eval sb2-init $sb2_init_args ) || return
        [[ $detach ]] || object_config_set "target:$snapshot" "$SNAPSHOT_TIME_KEY" "$(date -R)" \
            || return
        [[ ! $detach ]] || reset_machine_id target "$snapshot" || return
        object_config_set "target:$snapshot" "$SYNC_TO_HOST_KEY" "${sync:-0}" || return
        # Ensure snapshots can be removed by user
        set_object_mode "target:$snapshot" "$MODE_USER" || return
        maybe_synchronise_object "target:$snapshot" || return
    else
        local snapshot_of=
        snapshot_of=$(object_config_get "target:$snapshot" "$SNAPSHOT_OF_KEY") || return
        if [[ $snapshot_of != "$original" ]]; then
            echo >&2 "The '$snapshot' target is not a snapshot of '$original'"
            return 1
        fi

        if [[ $reset == force ]] || ([[ $reset == outdated ]] \
                && target_snapshot_needs_reset "$snapshot" "$original"); then
            echo >&2 "Resetting snapshot '$snapshot' of '$original' target..."
            local config=$(object_config_file "target:$snapshot")
            if [[ -e $config ]]; then
                read -r -d '' config_backup <"$config"
            fi
            reset_started=1
            sudo_sync_trees "$MER_TARGETS/$original" "$MER_TARGETS/$snapshot" || return
            if [[ $config_backup ]]; then
                cat >"$config" <<<"$config_backup"
                config_backup=
            fi
            object_config_set "target:$snapshot" "$SNAPSHOT_TIME_KEY" "$(date -R)" || return
            maybe_synchronise_object "target:$snapshot" || return
        elif target_snapshot_needs_reset "$snapshot" "$original"; then
            echo >&2 "Preserving outdated snapshot '$snapshot' of '$original' target..."
        fi
    fi

    if [[ ! $detach ]]; then
        # Let snapshots use original target's package cache dir, so that possible
        # cached packages survive snapshot reset.
        sed -i "$MER_TARGETS/$snapshot/etc/zypp/zypp.conf" \
            -e "/^\s*packagesdir\s*=/ {
                s,=.*,= /sdkroot$MER_TARGETS/$original/var/cache/zypp/packages,
                i # Overriden by sdk-manage
            }"
        # Unfortunately it's not easy to exclude those from syncing
        rm -rf "$MER_TARGETS/$snapshot/var/cache/zypp/packages"
    fi

    if [[ $INSIDE_VBOX ]]; then
        update_df_cache
    fi

    succeeded=1
}

target_snapshot_needs_reset() {
    local snapshot=$1 original=$2

    original_updated "$MER_TARGETS"/{"$original","$snapshot"}/etc/ssu/ssu.ini && return
    original_updated "$MER_TARGETS"/{"$original","$snapshot"}/etc/machine-id && return

    local snapshot_time=
    snapshot_time=$(object_config_get "target:$snapshot" "$SNAPSHOT_TIME_KEY") || return
    # Convert date to unix seconds
    snapshot_time=$(date --date "$snapshot_time" +%s) || return

    # Try to avoid querying RPM database as it takes considerable amount of time
    rpm_db_not_changed_since "$MER_TARGETS/$original/var/lib/rpm" "$snapshot_time" && return 1

    local most_recent_install_time=
    # TODO This has the unwanted side effect of rebuilding/cleaning the RPM database
    most_recent_install_time=$(set -o pipefail; rpm --root="$MER_TARGETS/$original" -qa \
        --queryformat '%{installtime}\n' |sort -n |tail -n1) || return
    # Compare values in unix seconds
    (( most_recent_install_time > snapshot_time )) && return

    # The rpm database does not know about the most recent package removal, so
    # parse the zypper install log. zypp/history looks like this:
    # 2019-02-27 16:39:18|remove |droid-hal-mydevice-tools|0.0.6-201902271629|armv7hl|user@pc|
    # But could also look like this:
    # 2019-02-27 16:39:18|install|remove-my-pkg|0.0.6-201902271629|armv7hl|user@pc|
    # And it can also contain comment lines - starting with '#'
    local most_recent_removal_time=$(set -o pipefail; \
        tac "$MER_TARGETS/$original/var/log/zypp/history" \
        |grep -v '^#' \
        |awk -F'|' '$2 == "remove" { print $1; exit; }'
    ) || return
    [[ $most_recent_removal_time ]] || return
    most_recent_removal_time=$(date --date "$most_recent_removal_time" +%s) || return
    # Compare values in unix seconds
    (( most_recent_removal_time > snapshot_time )) && return
}

reserve_target() {
    local reset_reused=outdated positional_args=()
    while [[ $# -gt 0 ]]; do
        case $1 in
            --reset-reused)
                reset_reused=outdated
                ;;
            --reset-reused=*)
                reset_reused=${1#*=}
                if [[ $reset_reused != @(soft|outdated|force) ]]; then
                    echo >&2 "unexpected argument to --reset-reused: '$reset_reused'"
                    usage >&2
                    return 1
                fi
                ;;
            -*)
                echo >&2 "unrecognized option '$1'"
                usage >&2
                return 1
                ;;
            *)
                if [[ ${#positional_args[*]} -eq 4 ]]; then
                    echo >&2 "unexpected positional argument '$1'"
                    usage >&2
                    return 1
                fi
                positional_args+=("$1")
                ;;
        esac
        shift
    done

    if [[ ${#positional_args[*]} -ne 4 ]]; then
        echo >&2 "Too few arguments given"
        usage >&2
        return 1
    fi

    local original=${positional_args[0]}
    local snapshot_template=${positional_args[1]}
    local lock_file=${positional_args[2]}
    local pool_size=${positional_args[3]}

    assert_name_valid "$original" || return
    assert_name_valid "$snapshot_template" || return

    if ! target_exists "$original"; then
        echo >&2 "Target not found '$original'"
        return 1
    fi

    assert_template_valid "$snapshot_template" || return

    if ! is_locked "$lock_file"; then
        echo >&2 "WARNING: File does not seem to be locked: '$lock_file'"
    fi

    if ! [[ $pool_size -gt 0 ]]; then
        echo >&2 "Not a positive integer: '$pool_size'"
        return 1
    fi

    local target= pooled_targets=() lru_pooled_target= lru_time= lru_same_lock= reset=
    for target in $(get_all_of_type_target); do
        name_matches_template "$target" "$snapshot_template" || continue

        local other_lock_file=
        other_lock_file=$(object_config_get "target:$target" "$LOCK_KEY") || return
        if [[ ! $other_lock_file ]]; then
            echo >&2 "WARNING: Ingoring pooled target with no lock file set: '$target'"
            continue
        fi

        pooled_targets+=("$target")

        # Target currently in use
        [[ $other_lock_file == $lock_file ]] || ! is_locked "$other_lock_file" || continue

        local lock_time=
        lock_time=$(object_config_get "target:$target" "$LOCK_TIME_KEY") || return
        lock_time=$(date --date "$lock_time" +%s) || return

        # Target recently reserved using the same lock is favored disregards its LRU score
        if [[ $other_lock_file == "$lock_file" ]]; then
            lru_pooled_target=$target
            lru_time=$lock_time
            lru_same_lock=1
            reset=$reset_reused
            break
        fi

        if [[ ! $lru_time || $lru_time -gt $lock_time ]]; then
            lru_pooled_target=$target
            lru_time=$lock_time
            reset=force
        fi
    done

    local snapshot=
    if [[ $lru_pooled_target && ($lru_same_lock || ${#pooled_targets[@]} -ge $pool_size) ]]; then
        snapshot=$lru_pooled_target
        snapshot_target --reset="$reset" "$original" "$snapshot" || return
    else
        if [[ ${#pooled_targets[@]} -ge $pool_size ]]; then
            echo >&2 "Pool size exceeded. Target cannot be reserved."
            return 1
        fi

        local snapshot=
        snapshot=$(IFS=$'\n'; random_name "$snapshot_template" \
            <<<"${pooled_targets[*]:+${pooled_targets[*]}}") || return

        # sb2-init would pollute stdout
        snapshot_target "$original" "$snapshot" >&2 || return
    fi

    object_config_set "target:$snapshot" "$LOCK_TIME_KEY" "$(date -R)" || return
    object_config_set "target:$snapshot" "$LOCK_KEY" "$lock_file" || return

    if [[ $INSIDE_VBOX ]]; then
        update_df_cache
    fi

    echo "$snapshot"
}

clone_target() {
    local positional_args=()
    while [[ $# -gt 0 ]]; do
        case $1 in
            -*)
                echo >&2 "unrecognized option '$1'"
                usage >&2
                return 1
                ;;
            *)
                if [[ ${#positional_args[*]} -eq 2 ]]; then
                    echo >&2 "unexpected positional argument '$1'"
                    usage >&2
                    return 1
                fi
                positional_args+=("$1")
                ;;
        esac
        shift
    done

    local original=${positional_args[0]}
    local clone=${positional_args[1]}

    if ! target_exists "$original"; then
        echo >&2 "Target not found '$original'"
        return 1
    fi

    if target_is_snapshot "$original"; then
        echo >&2 "Cannot clone a target which is a snapshot: '$original'"
        return 1
    fi

    if target_exists "$clone"; then
        echo >&2 "Target of this name already exists '$clone'"
        return 1
    fi

    # See install_target
    local no_snapshot=
    no_snapshot=$(object_config_get_boolean "target:$original" "$SYNC_TO_HOST_KEY") || return
    if [[ $no_snapshot ]]; then
        snapshot_target --detach "$original" "$clone" || return
    else
        snapshot_target --detach --no-sync "$original" "$clone" || return
        snapshot_target "$clone"{,."$DEFAULT_SNAPSHOT_SUFFIX"} || return
    fi

    if [[ $INSIDE_VBOX ]]; then
        local size=$(sudo du --summarize --bytes "$MER_TARGETS/$clone" |cut -f1)
        manage_user_targets_repository touch target "$clone" "${size:-0}"
    fi
}

remove_target() {
    local force=
    local target= snapshots_of=
    while [[ $# -gt 0 ]]; do
        case $1 in
            -f|--force)
                force=1
                ;;
            --snapshots-of)
                snapshots_of=1
                ;;
            *)
                if [[ $target ]]; then
                    echo >&2 "unexpected argument '$1'"
                    usage
                    return 1
                fi
                target=$1
                ;;
        esac
        shift
    done
    assert_name_valid "$target" || return
    local is_snapshot=$(target_is_snapshot "$target" && echo 1)
    if [[ -d $SBOX2DIR/$target ]]; then
        local mode=$(get_object_mode "target:$target")
        if [[ $MODE != "$mode" && ! $snapshots_of && ! $force ]]; then
            echo >&2 "The target '$target' can only be removed in the '$mode' mode."
            return 1
        fi

        local snapshots=$(get_snapshots_of_target "$target")
        if [[ $snapshots ]]; then
            if [[ $force || $snapshots_of ]]; then
                for snapshot in $snapshots
                do
                  remove_target ${force:+--force} "$snapshot" || return
                done
            else
                snapshots=$(echo $snapshots |sed 's/ /, /')
                echo >&2 "Cannot remove target '$target': The following snapshots "\
                    "exist: '$snapshots'. Remove the snapshots first."
                return 1
            fi
        fi

        [[ $snapshots_of ]] && return

        rm -r "$SBOX2DIR/$target"
        sudo rm -rf "$MER_TARGETS/$target"
        if [[ $INSIDE_VBOX && -e $HOST_TARGETS/$target ]]; then
            rm -rf "$HOST_TARGETS/$target"
            echo >&2 "Notifying Qt Creator of removed target"
        fi
    else
        echo >&2 "Note: target '$target' was not seen by sb2"
    fi

    if [[ $INSIDE_VBOX ]]; then
        update_df_cache

        # Notify Qt Creator always when we're in the VM to keep the
        # target information in sync
        updateQtCreatorTargets --delete --name "$target" --target-xml "$TARGETS_XML" || return
        if [[ $MODE == "$MODE_USER" && ! $is_snapshot ]]; then
            manage_user_targets_repository rm target "$target"
        fi
    fi
}

manage_targets() {
    if ! [[ ${1:-} ]]; then
        usage >&2
        return 1
    fi

    case $1 in
	?(--)list ) shift
            list_targets "$@"
	    ;;
	?(--)upgradable ) shift
            object_upgradable target "$@"
	    ;;
	?(--)install ) shift
            install_target "$@"
	    ;;
        ?(--)snapshot ) shift
            snapshot_target "$@"
            ;;
        ?(--)clone ) shift
            clone_target "$@"
            ;;
        ?(--)reserve ) shift
            reserve_target "$@"
            ;;
	?(--)remove ) shift
	    remove_target "$@"
	    ;;
	?(--)refresh ) shift
            refresh_objects target "$@"
	    ;;
	?(--)update ) shift
            upgrade_object target "$@"
	    ;;
	?(--)sync ) shift
            local target=${1:-} timestamp=${2:-}
            assert_name_valid "$target" || return
	    if [[ ! $INSIDE_VBOX ]]; then
	        echo >&2 "This operation is only valid for SDK in VirtualBox"
                return 1
	    fi
            maybe_synchronise_object "target:$target" ${timestamp:+"$timestamp"}
	    ;;
	?(--)import ) shift
            assert_name_valid "${1:-}" || return
	    if [[ ! $INSIDE_VBOX ]]; then
	        echo >&2 "This operation is only valid for SDK in VirtualBox"
                return 1
	    fi
	    import_target "$@"
	    ;;
	?(--)register ) shift
            register_object target "$@"
	    ;;
        ?(--)package-list ) shift
            list_object_packages target "$@"
            ;;
        ?(--)package-install ) shift
            manage_object_packages install target "$@"
            ;;
        ?(--)package-remove ) shift
            manage_object_packages remove target "$@"
            ;;
        ?(--)package-diff ) shift
            diff_object_packages target "$@"
            ;;
        ?(--)package-cache-prune ) shift
            prune_object_package_cache target "$@"
            ;;
        ?(--)uuidgen ) shift
            reset_machine_id target "$@"
            ;;
        ?(--)maintain ) shift
            maintain target "$@"
            ;;
	* )
            echo >&2 "unrecognized option '$1'"
            usage >&2
            return 1
	    ;;
    esac
}

################################################################
# SDK

get_sdk_version() {
    echo "Version not available"
}

sdk_status() {
    local err=0
    sdk_vbox_status || err=1
    return $err
}

sdk_vbox_status() {
    [[ $INSIDE_VBOX ]] || return 0
    local err=0

    systemctl --failed || err=1

    if ! sudo VBoxControl -nologo sharedfolder list | grep -q ' home$'; then
	echo "'home' shared folder is missing"
        err=1
    fi
    if ! sudo VBoxControl -nologo sharedfolder list | grep -q ' config$'; then
	echo "'config' shared folder is missing"
        err=1
    fi
    if ! sudo VBoxControl -nologo sharedfolder list | grep -q ' targets$'; then
	echo "'targets' shared folder is missing"
        err=1
    fi

    return $err
}

manage_sdk() {
    if ! [[ ${1:-} ]]; then
        usage >&2
        return 1
    fi

    case $1 in
	?(--)version )
	    get_sdk_version
	    ;;
	?(--)status )
	    sdk_status
	    ;;
	?(--)upgradable ) shift
            object_upgradable sdk "$@"
	    ;;
	?(--)upgrade ) shift
            upgrade_object sdk "$@"
	    ;;
	?(--)refresh )
            refresh_objects sdk "$@"
	    ;;
	?(--)register ) shift
            register_object sdk "$@"
	    ;;
	* )
            echo >&2 "unrecognized option '$1'"
            usage >&2
            return 1
	    ;;
    esac
}

################################################################
# utility

get_register_credentials() {
    local username= password= other=()

    while [[ ${1:-} ]]; do
        case $1 in
	    --user )
                [[ -n ${2:-} ]] || { usage >&2; return 1; }
                username=$2
                shift 2
		;;
	    --password )
                [[ -n ${2:-} ]] || { usage >&2; return 1; }
                password=$2
                shift 2
		;;
	    *)
		other=(${other[@]:+"${other[@]}"} "$1")
                shift
		;;
	esac
    done

    if [[ ! $username || ! $password ]] && ! tty --quiet; then
        echo >&2 "Missing --user and/or --password option"
        usage >&2
        return 1
    fi

    while [[ ! $username ]]; do
        read -p "Username: " username
    done
    while [[ ! $password ]]; do
        read -p "Password: " -s password
        echo >&2
    done

    print_array "$username" "$password" "$(print_array ${other[@]:+"${other[@]}"})"
}

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

INSIDE_CHROOT=1

INSIDE_VBOX=$([[ -e /etc/mer-sdk-vbox ]] && echo 1)

# exactly one must be true
if [[ $INSIDE_CHROOT$INSIDE_VBOX != 1 ]]; then
    echo >&2 "Internal error: Failed to determine type of SDK installation"
    exit 1
fi

MER_TARGETS=/srv/mer/targets
MER_TOOLINGS=/srv/mer/toolings
HOST_TARGETS=/host_targets
SBOX2DIR=${HOME}/.scratchbox2
TARGETS_XML=${HOST_TARGETS}/targets.xml
USER_TARGETS_REPOSITORY=${HOST_TARGETS}/targets.repository
CONFIG_FILE_UNPREFIXED=sdk-manage.conf
CONFIG_FILE=.${CONFIG_FILE_UNPREFIXED}
NON_INTERACTIVE=1

MODE_KEY=mode
MODE_USER=user
MODE_INSTALLER=installer
MODE=$MODE_USER

SYNC_TO_HOST_KEY=sync-to-host
SNAPSHOT_OF_KEY=snapshot-of
SNAPSHOT_TIME_KEY=snapshot-time

DEFAULT_SNAPSHOT_SUFFIX=default

LOCK_KEY=lock
LOCK_TIME_KEY=lock-time

TMPDIR_DOWNLOADS=/var/tmp

################################################################
# Main

if [[ ${SDK_MANAGE_DEBUG:-} ]]; then
    sdk_manage_global_vars=
    sdk_manage_global_vars=$(compgen -v)
    check_leaked_local_vars() {
        local current=$(compgen -v)
        local ignored=(-e 'FUNCNAME' -e 'BASH_.*')
        current=$(grep -v --line-regexp "${ignored[@]}" <<<"$current")
        local leaked=$(join -v 2 <(echo "$sdk_manage_global_vars") <(echo "$current"))
        if [[ $leaked ]]; then
            echo >&2 "DEBUG: These variables should be declared local to the functions they originated from: $leaked"
        else
            echo >&2 "DEBUG: No local variable leaked"
        fi
    }
    trap check_leaked_local_vars EXIT
fi

if [[ ${FLOCKER:-} != $0 ]]; then
    if ! flock -en "$0" true; then
        echo >&2 "sdk-manage: acquiring lock..."
    fi
    exec env FLOCKER="$0" flock --no-fork -e "$0" "$0" "$@" || :
fi

if ! [[ ${1:-} ]]; then
    usage >&2
    exit 1
fi

################################################################################
if [[ $1 != --self-test ]]; then  ###  M A I N  EXECUTION BEGINS HERE  #########
################################################################################

if [ -e "${XDG_CONFIG_HOME:-$HOME/.config}/$CONFIG_FILE_UNPREFIXED" ] ; then
    source "${XDG_CONFIG_HOME:-$HOME/.config}/$CONFIG_FILE_UNPREFIXED"
fi

while (( $# > 0)); do
    case $1 in
        -m|--mode)
            case ${2:-} in
                "$MODE_USER"|"$MODE_INSTALLER")
                    MODE=$2
                    ;;
                '')
                    echo >&2 "argument expected: '$1'"
                    usage >&2
                    exit 1
                    ;;
                *)
                    echo >&2 "not a valid mode: '$2'"
                    usage >&2
                    exit 1
                    ;;
            esac
            shift
            ;;
        --interactive)
            NON_INTERACTIVE=
            ;;
        --version)
            echo "$0, version 1.4.88"
            exit 0
            ;;
        -h|?(--)help)
            help
            exit 0
            ;;
        * )
            break
            ;;
    esac
    shift
done

case $1 in
    ?(--)tooling ) shift
        manage_toolings "$@"
	;;
    ?(--)target ) shift
	manage_targets "$@"
	;;
    ?(--)toolchain ) shift
	manage_toolchains "$@"
	;;
    ?(--)devel?(pkg) ) shift
	manage_develpkgs "$@"
	;;
    ?(--)sdk ) shift
	manage_sdk "$@"
	;;
    ?(--)refresh-all ) shift
        manage_targets refresh --all || exit
        manage_toolings refresh --all || exit
        [[ ${1:-} == --no-sdk ]] || manage_sdk refresh
	;;
    ?(--)register-all ) shift
        register_all "$@"
        ;;
    * )
        echo >&2 "unrecognized option '$1'"
        usage >&2
        exit 1
	;;
esac

##############################################################################
exit; fi ###  S E L F - T E S T  EXECUTION BEGINS HERE #######################
##############################################################################

some_failed=

expect()
{
    desc=$1
    actual=$2
    expected=$(cat)

    if [[ $actual != "$expected" ]]; then
        cat <<EOF
*** FAIL unexpected output:
  Test case: $desc
  Expected: {{{
$expected
}}}
  Actual: {{{
$actual
}}}
  Diff: {{{
$(diff <(cat <<<$expected) <(cat <<<$actual))
}}}

EOF
        some_failed=1
    fi
}

##############################################################################
# diff_package_lists

old_packages="\
a-removed 1.2-1 meego
b-unchanged 1.3.4-0 meego
c-updated 1.3.3-1 meego
d-updated-vendor-changed 1.2.10-3.1 meego
e-vendor-changed 1.2.8-2.3 meego
f-unchanged 3.2.1-5 meego
g-removed 1.2-1 meego
h-unchanged 1.2.3-4 meego
"

now_packages="\
b-unchanged 1.3.4-0 meego
c-updated 1.3.4-1 meego
d-updated-vendor-changed 1.2.10-3.2 (none)
e-vendor-changed 1.2.8-2.3 (none)
f-unchanged 3.2.1-5 meego
h-unchanged 1.2.3-4 meego
r-added 0.1-1 (none)
s-added 0.23-2.3 meego
"

expect diff_package_lists "$(diff_package_lists "$old_packages" "$now_packages")" <<'END'
Added:

r-added                   0.1-1
s-added                   0.23-2.3

Removed:

a-removed                 1.2-1
g-removed                 1.2-1

Replaced:

c-updated                 1.3.3-1 -> 1.3.4-1
d-updated-vendor-changed  1.2.10-3.1 -> 1.2.10-3.2  (vendor changed)
e-vendor-changed          1.2.8-2.3                 (vendor changed)
END

expect diff_package_lists "$(diff_package_lists "$old_packages" "$old_packages")" <<'END'
Added:

(none)

Removed:

(none)

Replaced:

(none)
END

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

if [[ $some_failed ]]; then
    echo "*** Some tests failed"
    exit 1
else
    echo "*** All tests passed"
    exit 0
fi
