#!/bin/sh -efu
#
# Copyright (C) 2020  BaseALT /basealt.ru/
#
# This file is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
#

PROG="${0##*/}"
PROG_VERSION=0.1.4

MOUNTPOINTS="/proc,/dev/pts"
CONPONENTS="classic"
DEFARCH="$(uname -m)"

. shell-quote

show_help()
{
	cat <<EOF
Usage: $PROG [options] list | dir | *.src.rpm ...

$PROG is a tool to separate up the contents of an SRPM package
into two following categories:

* included (used) files that are read during the build;
* skipped (unused) files.

The result can be used later with srpm-cleanup(1).
$PROG can also be used to verify the cleaned-up SRPMs.

Main options:

  -s[PATH], --skipped[=PATH]    output or write down the list of
                              files that are skipped during the build,
                              this is the default;

  -i[PATH], --included[=PATH]    output or write down the list of
                               files that are included (read) during
                               the build;

  -E, --abort-on-error    abort on first error;

  -f, --force             overwrite existing results (force rebuild
                          packages);

  -y, --verify            rebuild and check that files in the skiplist
                          are truncated; reads the skiplist, -i is
                          ignored;

  -t PCENT, --toolerance=PCENT    ignore PCENT non-truncated files
                                relative to the number of files in the
                              skiplist;

  -x XLIST, --exceptions=XLIST    when verify the skiplist ignore files
                                matching the patterns listed in XLIST;

  --nodetect              disable skipped files detection; useful
                          for mass rebuilds and for debugging purposes;
                          -s and -i are ignored;

  -O OUTDIR, --outdir=OUTDIR    copy the rebuilt RPMs to OUTDIR;

  -l LDIR, --logdir=LOGDIR    save Hasher logs into LOGDIR;

  --disttag=TAG           set %disttag TAG when building packages;

  --disttag-index=INDEX   set %disttag from INDEX when building
                          packages;

  --limit-LIMIT           limit the work plan by LIMIT packages;

  --checksum-patterns=PATTERNS    calculate SHA256 sums for files
                                matching PATTERNS;

  --sumdir=SUMDIR         write checksum files to SUMDIR;

  --verify-checksums      verify checksum files from SUMDIR;

  -q, --quiet             do not print any info while running;

  -v, --verbose           print more info while running;

  --dry-run[-run]         show Hasher or GNU Parallel's dry
                          run preview;

  --check-args            exit after checking the arguments;

  -V,--version            print program version and exit;

  -h,--help               show this text and exit.

Main options, related to GNU Parallel:

  --with-srpms[=FROM]     transfer SRPMs (from directory FROM) to
                          remote builders;

  --selfie                copy this script to remote builders;

  --with-nprofile=PROFILE   copy PROFILE to remote builders; %h in the
                            PROFILE is substituted with the builder's
                          SSH host name (as specified in -H);

  --remote-dir=DIR        working home on the remote builders;

  --cleanup               clean-up SRPMs, self and resulting lists
                          on remote builders;

  --joblog=JOBLOG         keep job log JOBLOG in order to --retry
                          and --resume;

  --resume                resume from the last unfinished job
                          (requires --joblog, implies -f);

  --resume-failed         retry all failed and resume from the last
                          unfinished job (requires --joblog, implies
                          -f);

  --retry-failed          retry all failed jobs (requires --joblog);

  --retries=N             try each failing job N times;

  --controlmaster         use ssh's ControlMaster option;

  --sshdelay=SECS         delay starting next remote job by SECS
                          seconds (which can be a fraction of 1 s);

  --parallel-opts=OPTS    pass the specified options to GNU Parallel;

  --rsync-opts=OPTS       additional options for rsync.

Builder configuration options:

  -C CONF, --apt-config=CONF    use --apt-config=CONF with Hasher;

  -R ADDR, --repo=ADDR      alternatively, configure Hasher to use
                            the repository at ADDR;

  -B DIR, --base=DIR        SRPM base directory;

  -n NUMS, --nums=NUMS      allowed Hasher slot (subconfig) numbers;

  -a ARCHES, --arch=ARCHES    switch architecture before package
                            rebuild, build package for multiple
                            architectures (comma and space serarated);

  -d DIR, --dir=DIR         Hasher working directory path;

  -m LIST, --mountpoints=LIST    use the given (possibly empty)
                               mountpoint list instead of the
                            default: $MOUNTPOINTS;

  -j N|N:M|:M|PROFILE, --nproc=N|N:M|:M|PROFILE    limit the number of
                                                 CPUs to be used by each
                            Hasher slot by N or in accordance with a
                            PROFILE file; if specified, M limits CPUs for
                            the %check section only; %h in the PROFILE
                            is substituted with the builder id (see --id
                            below); use --with-nprofile to transfer the
                            profiles to remote builders;

  --no-size                 don't try to measure and output the size
                            of Hasher workdir after a successful
                            build;

  --with-pre=PRESCRIPT      run PRESCRIPT <nvr.arch> <workdir> <slot> before
                            the Hasher run;

  --with-post=POSTSCRIPT    run POSTSCRIPT <nvr.arch> <workdir> <slot> after
                            the  Hasher run;

  --init-cmds=CMDS...       execute CMDS before the Hasher run;

  --without-check           rebuild with RPM %check stage disabled;

  --build-args=BARGS        additional arguments to rpmbuild;

  --no-query-repackage      don't pass --query-repackage to hsh;

  --max-silence=SECS        kill Hasher if no log is written in SECS
                            seconds;

  --debug                   do not delete temporary directory and print
                            its path on exit; implies -v|--verbose;

  --id=NAME                 builder name to print in log (normally --- the
                            builder's SSH host name as specified in -H);

  -e SSH, --ssh=SSH         use SSH remote shell to reach remote
                            builders (default is ssh);

  -u USER, --user=USER      name of a remote user to login with;

  -H REMOTES, --host=REMOTES    add remote builders and make any of
                              the above builder configuration options
                            specified after it specific to that
                            remotes.


For more information see hsh-separate-sources(1).
Report bugs to https://bugzilla.altlinux.org/.

EOF
}

print_version()
{
	cat <<EOF
$PROG version $PROG_VERSION
Written by: see the source for author info.

Copyright (C) 2020 BaseALT /basealt.ru/
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.
EOF
}

show_usage()
{
	cat <<EOF
Usage: $PROG [options] [*.src.rpm...]

Run $PROG -h to see the help page.
EOF
    return 1
}

# Early dry-run and debug detection
detect_debug() {
    while [ $# -gt 0 ]; do
        case "$1" in
            --debug|--dry-run|--dry-run-run)
                return 0
                ;;
        esac
        shift
    done

    return 1
}

if detect_debug "$@"; then
    echo "Command line: $*" >&2
fi


OPTS=`getopt -n $PROG -o C:,R:,B:,i::,s::,n:,a:,d:,m:,j:,e:,u:,H:,f,y,t:,x:,l:,O:,E,q,v,V,h \
             -l apt-config:,repo:,base:,included::,skipped::,nums:,arches:,dir:,mountpoints:,nproc:,ssh:,user:,host:,force,verify,exceptions:,outdir:,logdir:,abort-on-error,quiet,verbose,debug,id:,dry-run,dry-run-run,with-srpms::,selfie,with-nprofile:,remote-dir:,cleanup,joblog:,resume,resume-failed,retry-failed,retries:,controlmaster,sshdelay:,parallel-opts:,rsync-opts:,no-size,check-args,with-pre:,with-post:,init-cmds:,without-check,toolerance:,nodetect,build-args:,no-query-repackage,max-silence:,disttag:,disttag-index:,limit:,checksum-patterns:,sumdir:,verify-checksums,version,help -- "$@"` || show_usage
eval set -- "$OPTS"

# Builder configuration options:
_config=
_repos=
_base=
_nums=
_arches=
_dir=
_mountpoints="$MOUNTPOINTS"
_nproc=
_rshell=
_ruser=
_no_size=
_debug=
_id=
_with_pre=
_with_post=
_init_cmds=
_without_check=
_build_args=
_no_query_repackage=
_max_silence=

# Builder run options:
slot_sel=

reset_args() {
    config=
    repos=
    base=
    nums=
    arches=
    dir=
    mountpoints="$MOUNTPOINTS"
    nproc=
    rshell=
    ruser=
    no_size=
    debug=
    id=
    slot_sel=
    with_pre=
    with_post=
    init_cmds=
    without_check=
    build_args=
    no_query_repackage=
    max_silence=
}
reset_args

save_args() {
    _config="$config"
    _repos="$repos"
    _base="$base"
    _nums="$nums"
    _arches="$arches"
    _dir="$dir"
    _mountpoints="$mountpoints"
    _nproc="$nproc"
    _rshell="$rshell"
    _ruser="$ruser"
    _no_size="$no_size"
    _debug="$debug"
    _id="$id"
    _with_pre="$with_pre"
    _with_post="$with_post"
    _init_cmds="$init_cmds"
    _without_check="$without_check"
    _build_args="$build_args"
    _no_query_repackage="$no_query_repackage"
    _max_silence="$max_silence"
}

# Multi-host execution:
next_remotes=
next_args=
base_args=
remotes=
total_arches=
total_slots=0

# GNU Parallel specific
joblog=
resume=
resume_failed=
retry_failed=
retries=
controlmaster=
sshdelay=
parallel_opts=
rsync_opts=

# Main options:
included=
skipped=
stdout='skipped'
aerr=
force=
quiet=
verbose=
dry_run=
with_srpms=
fromdir=
selfie=
with_nprofile=
remotedir=
cleanup=
check_args_only=
cleanup_hasher=1
verify=
exceptions=
outdir=
toolerance=
logdir=
nodetect=
disttag=
disttag_index=
limit=
checksums=
sumdir=
sumverify=

print_error() {
    local fmt="${1:-}"; shift
    printf "${id:+[$id]:}$fmt\\n" "$@" >&2
}

print_info() {
    [ -n "$quiet" ] || print_error "$@"
}

append_filtered() {
    local vals="$1"
    local val="$2"
    local grepopts="${3:--wF}"
    local sepr="${4:- }"

    if [ -z "$vals" ]; then
        vals="$val"
    elif ! echo "$vals" | grep -q $grepopts "$val"; then
        vals="$vals$sepr$val"
    fi

    echo "$vals"
}

split_optval() {
    echo "$1" | sed -e 's/[,[:space:]]\+/ /g'
}

_export_args() {
    [ -z "$config" ] || echo -n " -C \"$(quote_shell "$config")\""
    [ -z "$repos" ] || echo -n " -R \"$(quote_shell "$repos")\""
    [ -z "$base" ] || echo -n " -B \"$(quote_shell "$base")\""
    [ -z "$nums" ] || echo -n " -n \"$(quote_shell "$nums")\""
    # Option -a <arch> is substituted by GNU Parallel from the input
    # parameter and thus not exported.
    [ -z "$dir" ] || echo -n " -d \"$(quote_shell "$dir")\""
    [ "$mountpoints" = "$MOUNTPOINTS" ] || \
        echo -n " -m \"$(quote_shell "$mountpoints")\""
    [ -z "$nproc" ] || echo -n " -j \"$(quote_shell "$nproc")\""
    [ -z "$no_size" ] || echo -n " --no-size"
    [ -z "$debug" ] || echo -n " --debug"
    [ -z "$id" ] || echo -n " --id=\"$(quote_shell "$id")\""
    if [ -n "$with_pre" ]; then
        echo -n " --with-pre=\"$(quote_shell "$with_pre")\""
    fi
    if [ -n "$with_post" ]; then
        echo -n " --with-post=\"$(quote_shell "$with_post")\""
    fi
    if [ -n "$init_cmds" ]; then
        echo -n " --init-cmds=\"$(quote_shell "$init_cmds")\""
    fi
    [ -z "$without_check" ] || echo -n " --without-check"
    [ -z "$build_args" ] || echo -n " --build-args=\"$(quote_shell "$build_args")\""
    [ -z "$no_query_repackage" ] || echo -n " --no-query-repackage"
    [ -z "$max_silence" ] || echo -n " --max-silence=\"$(quote_shell "$max_silence")\""
    echo
}
export_args() {
    local args="$(_export_args)"
    echo "${args# }"
}

_export_main_args() {
    case "$stdout" in
        skipped)
            if [ -n "$included" ]; then
                echo -n " -s"
            fi
            ;;
        included)
            echo -n " -i"
            ;;
    esac

    [ -z "$included" ] || echo -n " -i\"$(quote_shell "$included")\""
    [ -z "$skipped" ] || echo -n " -s\"$(quote_shell "$skipped")\""
    [ -z "$force" ] || echo -n " -f"
    [ -z "$quiet" ] || echo -n " -q"
    [ -z "$verbose" ] || echo -n " -v"
    [ -z "$verify" ] || echo -n " -y"
    [ -z "$nodetect" ] || echo -n " --nodetect"
    [ -z "$exceptions" ] || echo -n " -x \"$(quote_shell "${exceptions##*/}")\""
    [ -z "$outdir" ] || echo -n " -O \"$(quote_shell "$outdir")\""
    [ -z "$toolerance" ] || echo -n " -t \"$(quote_shell "$toolerance")\""
    [ -z "$logdir" ] || echo -n " -l \"$(quote_shell "$logdir")\""
    [ -z "$disttag" ] || echo -n " --disttag=\"$(quote_shell "$disttag")\""
    [ -z "$disttag_index" ] || echo -n " --disttag-index=\"$(quote_shell "${disttag_index##*/}")\""
    [ -z "$checksums" ] || echo -n " --checksum-patterns=\"$(quote_shell "$checksums")\""
    [ -z "$sumdir" ] || echo -n " --sumdir=\"$(quote_shell "$sumdir")\""
    [ -z "$sumverify" ] || echo -n " --verify-checksums"
    echo
}
export_main_args() {
    local args="$(_export_main_args "$@")"
    echo "${args# }"
}

export_all_args() {
    local args="$(export_args)"
    [ -z "$args" ] || args="$args "
    args="$args$(export_main_args)"
    echo "$args"
}

check_args() {
    local local_args="$(export_args)"

    local ret=0
    eval "$0" --check-args $base_args $local_args || ret=$?

    if [ $ret -eq 0 ]; then
        echo "$local_args"
    fi

    return $ret
}

configure_remote() {
    local remote="$1"
    local next_args="$2"

    local allarches="$_arches"
    for arch in $arches; do
        allarches="$(append_filtered "$allarches" "$arch")"
    done

    local grps=
    for arch in $allarches; do
        [ -z "$grps" ] || grps="$grps+"
        grps="$grps$arch"
    done

    local allnums="$_nums"
    for num in $nums; do
        allnums="$(append_filtered "$allnums" "$num")"
    done

    local slot_count=$(echo "$allnums" | wc -w)
    [ $slot_count -gt 0 ] || slot_count=1

    if [ -n "$ruser" ] &&
           echo "$remote" | grep -q '^[A-Za-z0-9_-]\+$'
    then
        remote="$ruser@$remote"
    fi

    if [ -n "$rshell" ]; then
        remote="$rshell $remote"
    fi

    remote="${grps:+@$grps/}$slot_count/$remote"
    if [ -z "$_arches$arches" ]; then
        print_error "WARNING: %s is expected to build any arch!" \
                    "$remote"
    fi

    echo "$remote${next_args:+ + $next_args}"
}

append_next_remotes() {
    local arch=
    local remote=
    if [ -n "$next_remotes" ]; then
        next_args="$(check_args)" || exit $?
        for remote in $next_remotes; do
            remote="$(configure_remote "$remote" "$next_args")"
            remotes="$(append_filtered "$remotes" "$remote" -xF "
")"
        done
        next_remotes=
        for arch in $arches; do
            total_arches="$(append_filtered "$total_arches" "$arch")"
        done
        local hostslots="$(echo "$nums" | wc -w)"
        [ "$hostslots" -gt 0 ] || hostslots=1
        total_slots=$((total_slots + hostslots))
    fi
}

while :; do
	case "$1" in
		-C|--apt-config)
            shift
            config="$1"
            ;;
		-R|--repo)
            shift
            repos="$(append_filtered "$repos" "$1" -xF "
")"
            ;;
		-B|--base)
            shift
            base="$1"
            if [ -n "$verify" ]; then
                with_srpms=1
                fromdir="$1"
            fi
            ;;
		-i|--included)
            shift
            included="$1"
            if [ -n "$included" ]; then
                stdout=
            else
                stdout='included'
            fi
            ;;
        -s|--skipped)
            shift
            skipped="$1"
            if [ -n "$skipped" ]; then
                stdout=
            else
                stdout='skipped'
            fi
            ;;
		-O|--outdir)
            shift
            outdir="$1"
            ;;
        -l|--logdir)
            shift
            logdir="$1"
            ;;
        --disttag)
            shift
            disttag="$1"
            ;;
        --disttag-index)
            shift
            disttag_index="$1"
            ;;
        -n|--nums)
            shift
            case "$1" in
                :[0-9]*)
                    slot_sel="${1#:}"
                    ;;
                *)
                    for range in $(split_optval "$1"); do
                        if echo "$range" | grep -q '^[0-9]\+-[0-9]\+$'; then
                            range="$(seq ${range%-*} ${range#*-})"
                        elif ! echo "$range" | grep -q '^[0-9]\+$'; then
                            print_error "ERROR: Invalid number or number range: %s" "$range"
                            exit 1
                        fi
                        for num in $range; do
                            nums="$(append_filtered "$nums" "$num")"
                        done
                    done
                    ;;
            esac
            ;;
        -a|--arches)
            shift
            for arch in $(split_optval "$1"); do
                arches="$(append_filtered "$arches" "$arch")"
            done
            ;;
        -d|--dir)
            shift
            dir="$1"
            ;;
        -m|--mountpoints)
            shift
            mountpoints="$1"
            ;;
        -j|--nproc)
            shift
            nproc="$1"
            ;;
        --with-pre)
            shift
            with_pre="$1"
            ;;
        --with-post)
            shift
            with_post="$1"
            ;;
        --init-cmds)
            shift
            init_cmds="$1"
            ;;
        --without-check)
            without_check=1
            ;;
        --build-args)
            shift
            build_args="$1"
            ;;
        --no-query-repackage)
            no_query_repackage=1
            ;;
        --max-silence)
            shift
            max_silence="$1"
            ;;
        -e|--ssh)
            shift
            rshell="$1"
            ;;
        -u|--user)
            shift
            ruser="$1"
            ;;
        -H|--host)
            shift
            if [ -z "$remotes" -a -z "$next_remotes" ]; then
                # first -H|--host
                base_args="$(export_all_args)"
                save_args
                total_arches="$arches"
            elif [ -n "$next_remotes" ]; then
                append_next_remotes
            fi
            for remote in $(split_optval "$1"); do
                next_remotes="$(append_filtered "$next_remotes" "$remote")"
            done
            reset_args
            ;;
        -f|--force)
            force=1
            ;;
        -E|--abort-on-error)
            aerr=1
            ;;
        -y|--verify)
            verify=1
            with_srpms=1
            fromdir="$base"
            ;;
		-x|--exceptions)
            shift
            exceptions="$1"
            ;;
		-t|--toolerance)
            shift
            toolerance="$1"
            ;;
        --nodetect)
            nodetect=1
            stdout=
            ;;
        -q|--quiet)
            quiet=1
            ;;
        -v|--verbose)
            verbose=1
            ;;
        --no-size)
            no_size=1
            ;;
        --debug)
            debug=1
            cleanup_hasher=
            verbose=1
            ;;
        --id)
            shift
            id="$1"
            if [ "$id" = ":" ]; then
                id='localhost'
            fi
            ;;
        --dry-run)
            dry_run=1
            cleanup_hasher=
            ;;
        --dry-run-run)
            dry_run=2
            cleanup_hasher=
            ;;
        --with-srpms)
            shift
            with_srpms=1
            fromdir="$1"
            base="$1"
            ;;
        --selfie)
            selfie=1
            ;;
        --with-nprofile)
            shift
            with_nprofile="$1"
            ;;
        --remote-dir)
            shift
            remotedir="$1"
            ;;
        --cleanup)
            cleanup=1
            ;;
        --check-args)
            check_args_only=1
            ;;
        --joblog)
            shift
            joblog="$1"
            ;;
        --resume)
            resume=1
            force=1
            ;;
        --resume-failed)
            resume_failed=1
            force=1
            ;;
        --retry-failed)
            retry_failed=1
            ;;
        --retries)
            shift
            retries="$1"
            ;;
        --controlmaster)
            controlmaster=1
            ;;
        --sshdelay)
            shift
            sshdelay="$1"
            ;;
        --parallel-opts)
            shift
            [ -z "$parallel_opts" ] || \
                parallel_opts="$parallel_opts "
            parallel_opts="$parallel_opts$1"
            ;;
        --rsync-opts)
            shift
            [ -z "$rsync_opts" ] || \
                rsync_opts="$rsync_opts "
            rsync_opts="$rsync_opts$1"
            ;;
        --limit)
            shift
            limit="$1"
            ;;
        --checksum-patterns)
            shift
            checksums="$1"
            ;;
        --sumdir)
            shift
            sumdir="$1"
            ;;
        --verify-checksums)
            sumverify=1
            ;;
		-V|--version)
            print_version
            exit 0
            ;;
		-h|--help)
            show_help
            exit 0
            ;;
        --)
            shift
            break
            ;;
		*)
            print_error "ERROR: Unrecognized option: %s" "$1"
            exit 1
            ;;
	esac
	shift
done

# Process the remotes, if any
if [ -n "$next_remotes" ]; then
    append_next_remotes
fi

if [ -n "$remotes" ]; then
    # Reset args
    reset_args
    debug="$_debug"
    arches="$total_arches"

    print_info "Base args: %s" "$base_args"
    print_info "Builders:\\n%s" "$(echo "$remotes" | sed -e 's/^.*$/  * &/')"
    [ -z "$arches" ] || print_info "Architectures total: %s" "$arches"
fi


## Process the options

numarches=$(echo "$arches" | wc -w)

if [ -n "$config" ]; then
    if [ -n "$repos" ]; then
        print_error "ERROR: -C|--apt-config and -R|--repo can not be used together."
        exit 1
    fi
    if [ $numarches -gt 1 ]; then
        case "$config" in
            *%s*)
                ;;
            *)
                print_error "ERROR: In order to use -C|--apt-config with more than one architecture the path should contain %%s."
                exit 1
                ;;
        esac
    fi
fi

if [ -z "$remotes" -a $numarches -gt 1 -a -z "$config" -a -z "$repos" ]
then
    print_error "ERROR: Specify -C|--apt-config or -R|--repo to use more than one architecture."
    exit 1
fi

if [ -z "$included" -a -z "$skipped" -a -z "$stdout" -a \
        -z "$verify" -a -z "$nodetect" ]
then
    print_error "ERROR: Specify at least one category to output using -i|--included, -s|--skipped or both."
    exit 1
fi

if [ -n "$nums" -a -z "$dir" ]; then
    print_error "ERROR: Specify a working directory with -d|--dir in order to use Hasher slots (subconfigs)."
    exit 1
fi

if [ -n "$exceptions" -a ! -r "$exceptions" ]; then
    print_error "ERROR: %s isn't readable." "$exceptions"
    exit 1
fi

if [ -n "$disttag_index" ]; then
    if [ ! -r "$disttag_index" ]; then
        print_error "ERROR: Unable to read the disttag index file %s." "$disttag_index"
        exit 1
    fi
fi

# Exit here if check args mode is specified
[ -z "$check_args_only" ] || exit 0


## Tempdir

tmpdir="$(mktemp -d --tmpdir $PROG.XXXX)"
if [ -z "$tmpdir" ]; then
    print_error "ERROR: Unable to create temporary directory."
    exit 2
fi

_global_hshpid=
onexit() {
    if [ -n "$tmpdir" ]; then
        if [ -z "$debug" ]; then
            rm -rf "$tmpdir"
        else
            echo "Leaving $tmpdir for debugging." >&2
        fi
    fi

    if [ -n "$_global_hshpid" ]; then
        kill "$_global_hshpid"
    fi
}
trap onexit EXIT INT


## Constructing globals

# Concatenate the inputs:
touch "$tmpdir/input"
while [ $# -gt 0 ]; do
    if [ -d "$1" ]; then
        find "$1" -maxdepth 1 -not -type d >>"$tmpdir/input"
    else
        case "$1" in
            *.src.rpm)
                echo "$1" >>"$tmpdir/input"
                ;;
            *)
                if [ -r "$1" ]; then
                    cat "$1" >>"$tmpdir/input"
                else
                    print_error "ERROR: Unable to read the list from %s." "$1"
                    exit 3
                fi
                ;;
        esac
    fi
    shift
done

sort -u "$tmpdir/input" >"$tmpdir/input.sorted"

if [ -n "$limit" ]; then
    print_info "Limit the work plan by %d packages as was asked." "$limit"
    head -n "$limit" "$tmpdir/input.sorted" >"$tmpdir/input"
else
    mv "$tmpdir/input.sorted" "$tmpdir/input"
fi
input="$tmpdir/input"

numpkgs=$(cat "$input" | wc -l)
numslots=$(echo "$nums" | wc -w)
numremotes=0
if [ -n "$remotes" ]; then
    numremotes=$(echo "$remotes" | wc -l)
fi

if [ $numpkgs -eq 0 ]; then
    print_info "Nothing to do. Exit."
    exit 0
else
    if [ -z "$slot_sel" ]; then
        print_info "%d packages in the list." "$numpkgs"
    fi
fi

multi=
if [ $numpkgs -gt 1 -o $numarches -gt 1 -o $numremotes -gt 0 ]
then
    multi=1
fi

if [ -n "$multi" ]; then
    if [ -z "$verify" ]; then
        if [ -n "$stdout" -a -z "$nodetect" ]; then
            print_error "ERROR: Can't use standard output when processing more than one package or architecture and in remote mode."
            exit 3
        fi
    else
        if [ -z "$skipped" -a -z "$nodetect" ]; then
            print_error "ERROR: Can't use standard input when processing more than one package or architecture and in remote mode."
            exit 3
        fi
    fi
fi

if [ -n "$verify" -a -z "$skipped" ]; then
    print_info "Reading the skiplist from stdin..."
    cat >"$tmpdir/ref_skiplist"
fi

# Generate APT configs from repository addresses:
if [ -n "$repos" ]; then
    for arch in ${arches:-"$DEFARCH"}; do
        cat <<EOF >"$tmpdir/apt.$arch.conf"
Dir::Etc::SourceList "$tmpdir/sources.$arch.list";
Dir::Etc::SourceParts "/var/empty";
EOF
        for repo in $repos; do
            case "$repo" in
                /*)
                    repo="file://$repo"
                    ;;
                *://*)
                    ;;
                *)
                    repo="file://$repo"
                    ;;
            esac
            cat <<EOF >>"$tmpdir/sources.$arch.list"
rpm $repo $arch $CONPONENTS
rpm $repo noarch $CONPONENTS
EOF
        done
    done
fi


## Functions

hshdir() {
    local num="${1:-}"
    if [ -z "$dir" ]; then
        return 0
    elif [ -z "$num" ]; then
        echo "$dir"
    else
        case "$dir" in
            *%d*)
                printf "$dir\\n" $num
                ;;
            *)
                echo "$dir/$num"
                ;;
        esac
    fi
}

defhshdir() {
    local wdir=
    if [ -e "$HOME/.hasher/config" ]; then
        wdir="$(sed -n -e 's/^workdir=//p' $HOME/.hasher/config)"
        if [ -n "$wdir" ]; then
            wdir="$(eval echo $wdir)" # FIXME
        fi
    fi

    [ -n "$wdir" ] || wdir="$HOME/hasher"

    echo "$wdir"
}

dryprint() {
    [ -z "$id" ] || echo -n "[$id]:"
    for arg in "$@"; do
        echo -n '"'
        quote_shell "$arg"
        echo -n '" '
    done
    echo
}

query_nproc() {
    local nprocs="${1:-$nproc}"
    local pkgname="${2:-}"

    nprocs="${nprocs/\%h/$id}"

    case "$nprocs" in
        [0-9]*)
            echo "$nprocs"
            ;;
        *)
            if [ -r "$nprocs" ]; then
                local def="$(grep '^[0-9]\+\(:[0-9]\+\)\?$' "$nprocs" | head -1)"
                if [ -n "$pkgname" ]; then
                    grep -v '^[0-9]\+\(:[0-9]\+\)\?$' "$nprocs" | (
                        IFS='='
                        rval=
                        while read -r pat val; do
                            if [ "$pat" = "$pkgname" -o \
                                 "$pat" = "${pkgname%-*}" -o \
                                 "$pat" = "${pkgname%-*-*}" ]
                            then
                                rval="$val"
                                break;
                            fi
                        done
                        echo "${rval:-$def}"
                    )
                else
                    echo "$def"
                fi
            else
                print_error "ERROR: CPU profile %s isn't readable." "$nprocs"
                return 1
            fi
            ;;
    esac
}

get_disttag() {
    local pkgname="$1"
    local dindex="${2:-$disttag_index}"

    if [ -n "$dindex" ]; then
        local dtag=
        if dtag="$(sed -n -e "/^${pkgname}[[:space:]]\\+/ { s/^[^[:space:]]\\+[[:space:]]\\+//; p; q0 }" -e '$ q1' "$dindex")"; then
            if [ "$dtag" = '(none)' ]; then
                dtag='%nil'
            fi
            echo "$dtag"
        else
            return 1
        fi
    else
        return 2
    fi
}

detect_ubt() {
    echo "$1" | sed -n -e 's/^.*\(\.\(M[0-9]\+[PC]\.[0-9]\+\|S[0-9]\+\(\.[0-9]\+\)\?\)\)\.src\.rpm$/\1/p'
}

# Beware of exec!
_hshrun() {
    local pkg="$1"
    local arch="${2:-}"
    local wdir="$3"
    local num="${4:-}"
    local pnproc="${5:-}"
    local dtag="${6:-}"

    local cnproc="${pnproc#*:}"
    if [ "$cnproc" != "$pnproc" ]; then
        pnproc="${pnproc%%:*}"
        cnproc="${cnproc%%:*}"
    fi

    local lbuild_args="$build_args"

    if [ -n "$cnproc" ]; then
        lbuild_args="$lbuild_args --define=\"__spec_check_custom_pre export NPROCS=$cnproc\""
    fi

    if [ -z "$nodetect" ]; then
        lbuild_args="$lbuild_args --define=\"__spec_build_custom_pre touch /usr/src/tmp/timestamp; touch /usr/src/tmp/skipped; touch /usr/src/tmp/included\""
        lbuild_args="$lbuild_args --define=\"__spec_autodep_custom_pre find /usr/src/RPM/BUILD -type f -not -anewer /usr/src/tmp/timestamp -a -not -empty | cut -d/ -f7-20  > /usr/src/tmp/skipped; find /usr/src/RPM/BUILD -type f -anewer /usr/src/tmp/timestamp | cut -d/ -f7-20  > /usr/src/tmp/included\""
    fi

    local ubt="$(detect_ubt "$pkg")"
    [ -n "$ubt" ] || ubt="%nil"
    lbuild_args="$lbuild_args --define=\"ubt $ubt\""

    if [ -n "$disttag" -o -n "$disttag_index" ]; then
        lbuild_args="$lbuild_args --define=\"disttag ${dtag:-%nil}\""
    fi

    if [ -n "$without_check" ]; then
        lbuild_args="$lbuild_args --disable check --without check --disable test"
    fi

    local wconfig="$config"

    if [ -n "$wdir" ]; then
        mkdir -p "$wdir" || return $?
    fi

    if [ -n "$wconfig" ]; then
        if [ -n "$arch" ]; then
            case "$wconfig" in
                *%s*)
                    wconfig="$(printf "$wconfig" "$arch")"
                    ;;
            esac
        fi
    elif [ -n "$repos" ]; then
        wconfig="$tmpdir/apt.${arch:-$DEFARCH}.conf"
    fi

    local query_repackage=1
    [ -z "$no_query_repackage" ] || query_repackage=

    no_dry_run=
    [ -n "$dry_run" ] || no_dry_run=1

    if [ -n "$init_cmds" ]; then
        eval "$init_cmds"
    fi

    ${dry_run:+dryprint}${no_dry_run:+exec} \
        ${arch:+setarch "$arch"} \
            hsh ${wdir:+$wdir} \
                ${num:+--number=$num} \
                ${mountpoints:+--mountpoints=$mountpoints} \
                ${pnproc:+--nproc=$pnproc} \
                --lazy-cleanup \
                --without-stuff \
                --no-wait-lock \
                --packager "Separ Ator <$PROG@altlinux.org>" \
                --build-args="$lbuild_args" \
                ${query_repackage:+--query-repackage} \
                ${wconfig:+--apt-config="$wconfig"} \
                ${arch:+--target=$arch} \
                "$pkg"
}

WAITDELAY=5
wait_for_hasher() {
    local timeout="${1:-$max_silence}"
    local hshpid="$2"
    local log="$3"
    local prefix="$4"

    if [ -n "$timeout" ] && [ "$timeout" -gt 0 ]; then
        local elapsed=0
        local waitdelay="${WAITDELAY:-5}"
        while [ ! -s "$log" -a "$elapsed" -lt "$timeout" ]; do
            if kill -0 "$hshpid" 2>/dev/null; then
                sleep "$waitdelay"
                elapsed=$((elapsed + waitdelay))
            else
                break;
            fi
        done

        if [ ! -s "$log" -a "$elapsed" -ge "$timeout" ]; then
            print_error "[%s] ERROR: Silence timeout of %d s reached" \
                        "$prefix" "$timeout"
            kill "$hshpid"
        else
            [ -z "$verbose" ] || \
                print_info "[%s] Build log started." "$prefix"
        fi
    fi

    wait "$hshpid" 2>/dev/null
}

hshrun() {
    local prefix="$1"; shift
    local log="$1"; shift

    local hshpid=
    (
        exec <&-
        export LANG=C
        _hshrun "$@" >"$log" 2>&1
    ) & hshpid=$!

    local ret=0
    _global_hshpid="$hshpid"
    wait_for_hasher "$max_silence" "$hshpid" "$log" "$prefix" || ret=$?
    _global_hshpid=

    return $ret
}

hshexec() {
    local num="${1:-}"; shift

    local wdir="$(hshdir "$num")"

    hsh-run ${wdir:+$wdir} \
            ${num:+--number=$num} \
            --no-wait-lock -- "$@"
}

hshcat() {
    local file="$1"
    local num="${2:-}"
    hshexec "$num" cat "$file"
}

hshcat_included() {
    local num="${1:-}"
    hshcat "/usr/src/tmp/included" "$num" 2>/dev/null ||:
}

hshcat_skipped() {
    local num="${1:-}"
    hshcat "/usr/src/tmp/skipped" "$num" 2>/dev/null ||:
}

hsh_cleanup() {
    local num="${1:-}"

    local wdir="$(hshdir "$num")"

    hsh ${wdir:+$wdir} \
        ${num:+--number=$num} \
        --no-wait-lock \
        --cleanup-only
}

check_create_dir() {
    local dir="$1"; shift

    dir="${dir%/}"

    if [ ! -e "$dir" ]; then
        mkdir -p "$dir"
    elif [ ! -d "$dir" ]; then
        print_error "ERROR: Can't use %s to write files into: not a directory." "$dir"
        exit 3
    fi

    echo "$dir"
}

check_create_file() {
    local file="$1"; shift

    if [ ! -e "$file" ]; then
        if [ "${file%/*}" != "$file" ]; then
            mkdir -p "${file%/*}"
        fi
        # Don't actualy create the file in order to make
        # have_results() make any sense.
    elif [ -d "$file" ]; then
        print_error "ERROR: Can't use %s for output: is a directory." "$file"
        exit 3
    fi
}

check_create_path() {
    local path="$1"; shift

    case "$path" in
        */)
            path="$(check_create_dir "$path")"
            ;;
        *)
            if [ ! -d "$path" ]; then
                check_create_file "$path"
            fi
            ;;
    esac
    echo "$path"
}

outfile_path() {
    local path="$1"
    local pkgname="$2"
    local arch="$3"
    local suf="$4"

    local outfile=
    if [ $numpkgs -gt 1 -o -d "$path" ]; then
        outfile="$path/$pkgname${arch:+.$arch}$suf"
    else
        outfile="$path"
    fi

    echo "$outfile"
}

process_logs() {
    local pkgname="$1"
    local arch="${2:-}"
    local num="${3:-}"

    local outbase="$tmpdir/$pkgname${arch:+.$arch}"
    local log="$outbase.log"
    local prefix="${num:+#$num }$pkgname${arch:+ $arch}"

    [ -z "$dry_run" ] || return 0

    local ret=0

    print_info "[%s] Processing build log..." "$prefix"
    grep '^patching file' "$log" | cut -d' ' -f3 | sed 's|^.*/\([^/]*/[^/]*\)$|\1|' >"$outbase.patched" ||:
    grep '^Patch.*(.*)' "$log" | sed 's/^.*(\(.*\)):/\1/' >>"$outbase.patched" ||:
    hshcat_included "$num" | sed 's|^.*/\([^/]*/[^/]*\)$|\1|' | sort -u >>"$outbase.patched" && \
        hshcat_skipped "$num" | grep -Fvf "$outbase.patched" | sort >"$outbase.skiplist" || ret=$?

    if [ $ret -eq 0 ]; then
        if [ -z "$verify" ]; then
            local outfile=
            if [ $ret -eq 0 -a -n "$included" ]; then
                outfile="$(outfile_path "$included" "$pkgname" "$arch" '.inclist')"
                hshcat_included "$num" >"$outfile" && \
                    print_info "[%s] Write the list of included files to %s." "$prefix" "$outfile" || ret=$?
            fi

            if [ $ret -eq 0 -a -n "$skipped" ]; then
                outfile="$(outfile_path "$skipped" "$pkgname" "$arch" '.skiplist')"
                cat "$outbase.skiplist" >"$outfile" && \
                    print_info "[%s] Write the list of skipped files to %s." "$prefix" "$outfile" || ret=$?
            fi

            if [ $ret -eq 0 -a -n "$stdout" ]; then
                case "$stdout" in
                    included)
                        hshcat_included "$num" || ret=$?
                        ;;
                    skipped)
                        if [ -z "$multi" ]; then
                            cat "$outbase.skiplist" || ret=$?
                        fi
                        ;;
                esac
            fi
        else
            if [ -n "$exceptions" ]; then
                [ -z "$verbose" ] || \
                    print_info "[%s] Using exceptions file %s" \
                               "$prefix" "$exceptions"
                grep -v -e '^\^' -e '\$$' "$exceptions" \
                     >"$tmpdir/exceptions.plain"
                grep -e '^\^' -e '\$$' "$exceptions" \
                     >"$tmpdir/exceptions.regexp"
                grep -Fvf "$tmpdir/exceptions.plain" "$outbase.skiplist" | \
                grep -vf "$tmpdir/exceptions.regexp" | \
                    grep -Fv -e '.gear' -e '.spec' | \
                         sort >"$outbase.skiplist.filtered" \
                         2>"$log.grep" ||:
            else
                grep -Fv -e '.gear' -e '.spec' "$outbase.skiplist" | \
                     sort >"$outbase.skiplist.filtered" \
                     2>"$log.grep" ||:
            fi
            if [ -s "$log.grep" ]; then
                cat "$log.grep" | prefix_out "${id:+[$id]:}[$prefix]" >&2
                print_error "[%s] ERROR: Unable to filter the skiplist." \
                            "$prefix"
                ret=1
            else
                local skiplist=
                if [ -d "$skipped" ]; then
                    skiplist="${skipped%/}/$pkgname.all.skiplist"
                else
                    skiplist="$skipped"
                fi

                if [ ! -r "$skiplist" ]; then
                    print_error "[%s] ERROR: Unable to read the reference skiplist %s." "$prefix" "$skiplist"
                    ret=1
                else
                    local skiplist_size="$(cat "$skiplist" | wc -l)"
                    local unskipped_count="$(sort "$skiplist" | comm -12 "$outbase.skiplist.filtered" - | wc -l)"
                    local errpcent=$((unskipped_count*100 / skiplist_size))
                    if [ $unskipped_count -eq 0 -o \
                         -n "$toolerance" -a \
                         "${toolerance:-0}" -ge "$errpcent" ]
                    then
                        print_info "[%s] Verification OK (%d%% of errors)" \
                                   "$prefix" "$errpcent"
                    else
                        print_error "[%s] ERROR: Verification error (%d%% errors), found %d non-truncated out of %d files listed in the reference %s:" \
                                    "$prefix" "$errpcent" \
                                    "$unskipped_count" "$skiplist_size" \
                                    "$skiplist"
                        ret=1
                        (
                            sort "$skiplist" | \
                                comm -12 "$outbase.skiplist.filtered" - | \
                                head -10
                            [ $unskipped_count -le 10 ] || echo "..."
                        ) | prefix_out "${id:+[$id]:}[$prefix]" >&2
                    fi
                fi
            fi
        fi
    fi

    if [ -z "$debug" ]; then
        rm -f "$outbase.patched" "$outbase.skiplist" \
              "$outbase.skiplist.filtered" "$log.grep"
    fi

    return $ret
}

have_results() {
    local pkgname="$1"
    local arch="${2:-}"

    if [ -n "$force" ]; then
        return 1
    fi

    if [ -z "$verify" -a -z "$nodetect" ]; then
        if [ -z "$included" -a -z "$skipped" ]; then
            return 1
        fi

        local outfile=
        if [ -n "$included" ]; then
            outfile="$(outfile_path "$included" "$pkgname" "$arch" '.inclist')"
            [ -e "$outfile" ] || return 1
        fi

        if [ -n "$skipped" ]; then
            outfile="$(outfile_path "$skipped" "$pkgname" "$arch" '.skiplist')"
            [ -e "$outfile" ] || return 1
        fi
    else
        local pkgoutdir="${outdir%/}/$pkgname${arch:+.$arch}"
        [ -e "$pkgoutdir/$pkgname.src.rpm" ] || return 1
    fi

    return 0
}

prefix_out() {
    sed -e "s/^.*\$/$1 &/" >&2
}

check_workdir() {
    local prefix="$1"
    local wdir="$2"

    if [ ! -d "$wdir" ]; then
        if ! mkdir -p "$wdir"; then
            print_error "[%s] ERROR: Unable to create workdir '%s'." \
                        "$prefix" "$wdir"
            return 1
        fi
    fi

    local ret=0
    local wfs=
    wfs="$(df --output=target "$wdir" | tail -1)" || ret=$?

    if [ $ret -eq 0 ]; then
        case "$wfs" in
            /*)
                ;;
            *)
                ret=2
                ;;
        esac
    fi

    if [ $ret -ne 0 ]; then
        print_error "[%s] ERROR: Unable to determine the filesystem of the workdir '%s'." "$prefix" "$wdir"
        return $ret
    fi

    ret=0
    local mopts=
    mopts="$(mount -l | sed -n -e "\|on[[:space:]]$wfs[[:space:]]type| { s/^.*(\\([^)]*\))\$/\\1/; p; q0 }" -e '$ q1')" || ret=$?

    if [ $ret -ne 0 -o -z "$mopts" ]; then
        print_error "[%s] ERROR: Unable to determine the mount options of the workdir '%s'." "$prefix" "$wdir"
        return 3
    fi

    print_info "[%s] Workdir is mounted on %s (%s)." "$prefix" "$wfs" "$mopts"

    (
        rw=
        IFS=,
        for mopt in $mopts; do
            case "$mopt" in
                rw)
                    rw=1
                    ;;
                noatime|relatime|nostrictatime|noexec)
                    print_error "[%s] ERROR: Incompatible mount option '%s' found for the workdir '%s'." "$prefix" "$mopt" "$wdir"
                    exit 4
                    ;;
            esac
        done

        if [ -z "$rw" ]; then
            print_error "[%s] ERROR: Required mount option '%s' not found for the workdir '%s'." "$prefix" 'rw' "$wdir"
            exit 5
        fi
    )
}

rebuild_one() {
    local pkg="$1"
    local arch="${2:-}"
    local num="${3:-}"

    local pkgname="${pkg##*/}"; pkgname="${pkgname%.src.rpm}"
    local log="$tmpdir/$pkgname${arch:+.$arch}.log"
    local prefix="${num:+#$num }$pkgname${arch:+ $arch}"

    if have_results "$pkgname" "$arch"; then
        print_info "[%s] Skip: no need to rebuild." "$prefix"
        return 0
    fi

    local pnproc=
    if [ -n "$nproc" ]; then
        pnproc="$(query_nproc "$nproc" "$pkgname")" || return $?
    fi

    local wdir=
    wdir="$(hshdir "$num")" || return $?
    wdir="${wdir:-$(defhshdir)}" || return $?

    local dtag=
    if [ -n "$disttag" ]; then
        dtag="$disttag"
    elif [ -n "$disttag_index" ]; then
        if [ -r "$disttag_index" ]; then
            dtag="$(get_disttag "$pkgname" "$disttag_index")" ||:
            if [ -z "$dtag" ]; then
                print_error "[%s] ERROR: Disttag not found." "$prefix"
                return 1
            fi
        else
            print_error "[%s] ERROR: Unable to read the disttag index file %s." "$prefix" "$disttag_index"
            return 1
        fi
    fi

    local ret=0
    (
        if [ -z "$dry_run" -a \( -n "$with_pre" -o -n "$with_post" \) ]
        then
            if [ -n "$wdir" ]; then
                mkdir -p "$wdir" || exit $?
            fi
            timestamp="$(printf '%08x\n' "$(date +%s)")"
            if [ -n "$with_post" ]; then
                trap "$with_post \"$pkgname.$arch\" \"$wdir\" \"$num\" \"$timestamp\" && print_info \"[%s] Post-script finished successfully.\" \"$prefix\"" EXIT INT
            fi
            if [ -n "$with_pre" ]; then
                if "$with_pre" "$pkgname.$arch" "$wdir" "$num" "$timestamp"
                then
                    print_info "[%s] Pre-script finished successfully" \
                               "$prefix"
                else
                    print_error "[%s] ERROR: Pre-script failed." \
                                "$prefix"
                    exit 255
                fi
            fi
        fi

        check_workdir "$prefix" "$wdir" || exit $?

        print_info "[%s] Start rebuilding%s..." "$prefix" \
                   ${pnproc:+" (nproc=$pnproc)"}

        if [ -n "$disttag" -o -n "$disttag_index" ]; then
            print_info "[%s] Using %%disttag %s" \
                       "$prefix" "${dtag:-%nil}"
        fi

        ret=0
        hshrun "$prefix" "$log" "$pkg" "$arch" "$wdir" "$num" "$pnproc" "$dtag" || ret=$?

        [ $ret -ne 255 ] || ret=1
        exit $ret
    ) || ret=$?

    if [ -n "$dry_run" ]; then
        cat "$log" >&2
    fi

    if [ $ret -eq 0 ]; then
        print_info "[%s] Successfully finished rebuilding." "$prefix"
    elif [ $ret -ne 255 ]; then
        if [ -z "$dry_run" -a -z "$quiet" ]; then
            tail "$log" | prefix_out "${id:+[$id]:}[$prefix]" >&2
        fi
        print_error "[%s] ERROR: Unsuccessfully finished rebuilding." "$prefix"
    fi

    if [ $ret -eq 0 ]; then
        if [ -z "$no_size" ]; then
            if [ -n "$wdir" ]; then
                LANG=C /usr/bin/du -cs "$wdir/aptbox" "$wdir/cache" "$wdir/chroot" 2>/dev/null | tail -1${verbose:+0} | prefix_out "${id:+[$id]:}[$prefix]"
            fi
        fi

        if [ -z "$nodetect" ]; then
            process_logs "$pkgname" "$arch" "$num" || ret=$?

            if [ $ret -eq 0 ]; then
                print_info "[%s] Done processing log." "$prefix"
            else
                print_error "[%s] ERROR: Log processing error." "$prefix"
            fi
        fi
    fi

    if [ $ret -eq 0 -a -n "$sumverify" ]; then
        if [ ! -r "${sumdir:+${sumdir%/}/}$pkgname${arch:+.$arch}.sums" ]; then
            print_error "[%s] ERROR: Unable to read the checksum file." "$prefix"
            ret=1
        else
            cat "${sumdir:+${sumdir%/}/}$pkgname${arch:+.$arch}.sums" | hshexec "$num" sha256sum -c 1>"${sumdir:+${sumdir%/}/}$pkgname${arch:+.$arch}.sumcheck" 2>&1 || ret=$?
            if [ $ret -eq 0 ]; then
                print_info "[%s] Checksum verification OK." "$prefix"
            else
                print_error "[%s] ERROR: Checksum verification failed." \
                             "$prefix"
            fi
        fi
    elif [ $ret -eq 0 -a -n "$checksums" ]; then
        eval "hshexec '$num' find /usr/src/RPM/BUILD \\( -name '$(echo "$checksums" | sed -e 's/^[[:space:]]\+//' -e 's/[[:space:]]\+$//' -e 's/[[:space:]]\+/'\'' -o -name '\''/g')' \\) -exec sha256sum '{}' \\;" >"$tmpdir/$pkgname${arch:+.$arch}.sums" || ret=$?
        if [ $ret -ne 0 ]; then
            print_error "[%s] ERROR: Failed to calculate the checksums." \
                        "$prefix"
        else
            [ -z "$sumdir" ] || mkdir -p "${sumdir%/}"
            cat "$tmpdir/$pkgname${arch:+.$arch}.sums" >"${sumdir:+${sumdir%/}/}$pkgname${arch:+.$arch}.sums" || ret=$?
            if [ $ret -ne 0 ]; then
                print_error "[%s] ERROR: Failed to copy the checksum file." \
                             "$prefix"
            fi
        fi
    fi

    local exp_ok="$tmpdir/$pkgname${arch:+.$arch}.exported"
    if [ $ret -eq 0 -a -n "$outdir" ]; then
        if [ -z "$wdir" ]; then
            print_error "[%s] ERROR: Unable to export RPMs: can't detect Hasher working directory." "$prefix"
            ret=1
        elif [ ! -d "${wdir%/}/repo" ]; then
            print_error "[%s] ERROR: Unable to export RPMs: directory %s doesn't exist." "$prefix" "${wdir%/}/repo"
            ret=1
        else
            local pkgoutdir="${outdir%/}/$pkgname${arch:+.$arch}"
            print_info "[%s] Copying the resulting RPMs to %s..." \
                       "$prefix" "${pkgoutdir%/}/"
            rm -f "$exp_ok"
            mkdir -p "$pkgoutdir" 2>"$log.copy" || ret=$?
            if [ $ret -eq 0 ]; then
                find "${wdir%/}/repo" -name '*.rpm' \
                     -exec cp -pLf '{}' "$pkgoutdir/" \; -a \
                     -exec touch "$exp_ok" \; \
                   2>>"$log.copy" || ret=$?
            fi
            if [ $ret -ne 0 -o ! -e "$exp_ok" ]; then
                print_error "[%s] ERROR: Failed to copy RPMs to %s." \
                            "$prefix" "$pkgoutdir/"
                cat "$log.copy" | \
                    prefix_out "${id:+[$id]:}[$prefix]" >&2
            fi
        fi
    fi

    if [ -n "$cleanup_hasher" ]; then
        print_info "[%s] Cleaning-up hasher..." "$prefix"
        local cret=0
        hsh_cleanup "$num" >>"$log" 2>&1 || cret=$?
        if [ $cret -eq 0 ]; then
            print_info "[%s] Done hasher cleanup." "$prefix"
        else
            print_error "[%s] ERROR: Hasher cleanup failed." \
                        "$prefix"
            if [ -z "$quiet" ]; then
                tail "$log" | \
                    prefix_out "${id:+[$id]:}[$prefix]" >&2
            fi
        fi
        [ $ret -ne 0 ] || ret=$cret
    fi

    if [ -n "$logdir" -a -z "$dry_run" ]; then
        mkdir -p "${logdir%/}" ||:
        cat "$log" >"${logdir%/}/$pkgname${arch:+.$arch}.log" ||:
    fi

    [ -n "$debug" ] || rm -f "$log" "$log.copy" "$exp_ok"

    return $ret
}


## Main

if [ -z "$dry_run" -a -z "$verify" ]; then
    if [ -n "$multi" -o -n "$slot_sel" ]; then
        [ -z "$included" ] || included="$(check_create_dir "$included")"
        [ -z "$skipped" ] || skipped="$(check_create_dir "$skipped")"
    else
        [ -z "$included" ] || included="$(check_create_path "$included")"
        [ -z "$skipped" ] || skipped="$(check_create_path "$skipped")"
    fi
fi

quote_remotes() {
    echo "$remotes" | (
        sshlogins=
        while read -r remote; do
            [ -z "$sshlogins" ] || sshlogins="$sshlogins "
            sshlogins="${sshlogins}-S \"$(quote_shell "$remote")\""
        done
        echo "$sshlogins"
    )
}

print_and_run() {
    if [ -n "$dry_run" ]; then
        echo "$@" >&2
    fi
    eval "$@"
}

list_arches() {
    local pkgarches="$(echo "$1" | sed 's/^.*\.\([^.]\+\)\.skiplist$/\1/' | tr '\n' ' ')"
    echo "${pkgarches% }"
}

basefile_path() {
    local fpath="$1"

    [ -n "$fpath" ] || return 0

    case "$fpath" in
        /*)
            fpath="${fpath%/*}/./${fpath##*/}"
            ;;
        ./*)
            fpath="$PWD/$fpath"
            ;;
        *)
            fpath="$PWD/./$fpath"
            ;;
    esac

    echo "$fpath"
}

if [ -n "$multi" -a \( "$numslots" -gt 1 -o "$numremotes" -gt 0 \) ]
then
    # Translate arguments for GNU Parallel
    dry_run_run=
    if [ "${dry_run:-0}" -gt 1 ]; then
        dry_run_run=1
        dry_run=
    fi

    no_dry_run=
    [ -n "$dry_run" ] || no_dry_run=1

    no_dry_run_run=
    [ -n "$dry_run_run" ] || no_dry_run_run=1

    no_verify=
    [ -n "$verify" ] || no_verify=1

    if [ "$numremotes" -gt 0 ]; then
        print_info "Activate parallel mode for %d hosts, %d slots" \
                   "$numremotes" "$total_slots"
        nums=
    else
        print_info "Activate parallel mode for %d slots" "$numslots"
    fi

    if [ -z "$base_args" ]; then
        base_args="$(export_all_args)"
    fi

    self="$0"
    basefile=
    if [ -n "$selfie" ]; then
        case "$self" in
            /*)
                basefile="${self%/*}/./${self##*/}"
                self="./${self##*/}"
                ;;
            *)
                basefile="$self"
                ;;
        esac
    fi

    print_info "Constructing the todo list..."
    cat "$input" | ( \
        skipped_builds=0
        while read -r pkg; do
            pkgname="${pkg##*/}"; pkgname="${pkgname%.src.rpm}"
            pkgarches=
            if [ -z "$verify" ]; then
                pkgarches="$arches"
            else
                if [ -d "$skipped" ]; then
                    pkgarches="$(list_arches "$(find "$skipped" -mindepth 1 -maxdepth 1 -not -type d -a -name "$pkgname.*.skiplist" -a -not -name '*.all.skiplist')")"
                else
                    pkgarches="$(list_arches "$skipped")"
                fi
                if [ -n "$arches" -a -n "$pkgarches" ]; then
                    _pkgarches=
                    for arch in $pkgarches; do
                        if echo "$arches" | grep -qwF "$arch"; then
                            [ -z "$_pkgarches" ] || \
                                _pkgarches="$_pkgarches "
                            _pkgarches="${_pkgarches}$arch"
                        fi
                    done
                    pkgarches="$_pkgarches"
                fi
            fi
            for arch in ${pkgarches:-""}; do
                prefix="$pkgname${arch:+ $arch}"
                if have_results "$pkgname" "$arch"; then
                    [ -z "$verbose" ] || \
                        print_info "[%s] Skip: no need to rebuild." \
                                   "$prefix"
                    skipped_builds=$((skipped_builds + 1))
                    echo "$pkgname" >>"$tmpdir/already-done-pkgs"
                else
                    echo "${arch}:::$pkg${arch:+${remotes:+@$arch}}"
                fi
            done
        done
        if [ $skipped_builds -gt 0 ]; then
            skipped_pkgs="$(sort -u "$tmpdir/already-done-pkgs" | wc -l)"
            print_info "%d builds of %d packages skipped: no need to rebuild." \
                       "$skipped_builds" "$skipped_pkgs"
        fi
    ) >"$input.todo"
    input="$input.todo"
    print_info "Done constructing the todo list."

    todosize="$(cat "$input" | wc -l)"
    if [ "$todosize" -eq 0 ]; then
        print_info "Nothing to do. Exit."
        exit 0
    else
        print_info "%d builds to run." "$todosize"
    fi

    nprofile=
    if [ -n "$with_nprofile" ]; then
        nprofile="${with_nprofile/\%h/\{host\}}"
    fi

    exceptions="$(basefile_path "$exceptions")"
    disttag_index="$(basefile_path "$disttag_index")"
    nprofile="$(basefile_path "$nprofile")"

    basefile_opt=
    case "$remotedir" in
        ...)
            basefile_opt=--transferfile
            ;;
        *)
            basefile_opt=--basefile
            ;;
    esac

    print_and_run \
    parallel_alt --will-cite \
             ${dry_run:+--dry-run} \
             ${nums:+-j $numslots} \
             ${aerr:+--halt soon,fail=1} \
             ${joblog:+--joblog "\"$(quote_shell "$joblog")\""} \
             ${resume:+--resume} \
             ${resume_failed:+--resume-failed} \
             ${retry_failed:+--retry-failed} \
             ${retries:+--retries "\"$(quote_shell "$retries")\""} \
             ${controlmaster:+--controlmaster} \
             ${sshdelay:+--sshdelay "\"$(quote_shell "$sshdelay")\""} \
             --line-buffer \
             ${remotes:+--plus --hostgroups --rsync-opts -azR $rsync_opts} \
             ${remotes:+$(quote_remotes)} \
             ${remotes:+${no_dry_run:+${basefile:+$basefile_opt "\"$(quote_shell "$basefile")\""}${exceptions:+ $basefile_opt "\"$(quote_shell "$exceptions")\""}}} \
             ${remotes:+${no_dry_run:+${disttag_index:+$basefile_opt "\"$(quote_shell "$disttag_index")\""}}} \
             ${remotes:+${no_dry_run:+${nprofile:+$basefile_opt "\"$(quote_shell "$nprofile")\""}}} \
             ${remotes:+${no_dry_run_run:+${with_srpms:+ --transferfile "\"$(quote_shell "${fromdir:+${fromdir%/}/}{2}")\""}${logdir:+ --return "\"$(quote_shell "${logdir%/}/{=2 s:^.*/::, s:\.src\.rpm\$:: =}${arches:+.{1\}}.log")\""}${checksums:+ --return "\"$(quote_shell "${sumdir:+${sumdir%/}/}{=2 s:^.*/::, s:\.src\.rpm\$:: =}${arches:+.{1\}}.sums")\""}${sumverify:+ --transferfile "\"$(quote_shell "${sumdir:+${sumdir%/}/}{=2 s:^.*/::, s:\.src\.rpm\$:: =}${arches:+.{1\}}.sums")\"" --return "\"$(quote_shell "${sumdir:+${sumdir%/}/}{=2 s:^.*/::, s:\.src\.rpm\$:: =}${arches:+.{1\}}.sumcheck")\""}}} \
             ${remotedir:+--workdir "\"$(quote_shell "$remotedir")\""} \
             ${cleanup:+--cleanup} \
             ${no_verify:+${no_dry_run_run:+${remotes:+${included:+ --return "\"$(quote_shell "$included${multi:+/{=2 s:^.*/::, s:\.src\.rpm\$:: =\}${arches:+.{1\}}.inclist}")\""}${skipped:+ --return "\"$(quote_shell "$skipped${multi:+/{=2 s:^.*/::, s:\.src\.rpm\$:: =\}${arches:+.{1\}}.skiplist}")\""}}}} \
             ${verify:+${no_dry_run_run:+${remotes:+${skipped:+--transferfile "\"$(quote_shell "${skipped%/}${multi:+/{=2 s:^.*/::, s:\.src\.rpm\$:: =\}.all.skiplist}")\""}}}} \
             ${outdir:+${no_dry_run_run:+${remotes:+--return "\"$(quote_shell "${outdir%/}/{=2 s:^.*/::, s:\.src\.rpm\$:: =}${arches:+.{1\}}")\""}}} \
             -C ::: \
             ${parallel_opts:+$parallel_opts} \
             eval "\"\\\"'$self'${dry_run_run:+ --dry-run} $(quote_shell $(quote_shell "$base_args"))${remotes:+ --id \\\\\\\"{host\}\\\\\\\" {localargs\}} ${nums:+-n :{%\}}${remotes:+-n :{localslot\}} ${arches:+-a {1\}} {2}\\\"\"" \
             :::: "\"$input\""

    exit $?
fi

# Selecting a slot if :N was passed
num=
if [ -n "$nums" ]; then
    num="$(echo "$nums" | cut -f ${slot_sel:-1} -d' ')"
    if [ -z "$num" ]; then
        print_error "BUG: Unable to select slot"
        exit 10
    fi
fi

ret=0
while read -r pkg; do
    pkg="${base:+$base/}$pkg"

    if [ ! -r "$pkg" ]; then
        print_error "ERROR: File is not accessible: %s." "$pkg"
        if [ "$numpkgs" -gt 1 ]; then
            if [ -n "$aerr" ]; then
                print_info "Abort on error as was asked."
                exit 3
            else
                continue
            fi
        else
            exit 3
        fi
    fi

    for arch in ${arches:-''}; do
        ret=0
        rebuild_one "$pkg" "$arch" "$num" || ret=$?
        if [ $ret -ne 0 -a -n "$aerr" ]; then
            print_info "Abort on error as was asked."
            exit $ret
        fi
    done
done <"$input"

exit $ret
