Blob Blame History Raw
#! /bin/bash

# Generate SRPM against Copr's dist-git instance.
# Copyright (C) 2017 Red Hat, Inc.
#
# This program 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.

#################################
##### BACKEND / BUILDER API #####
#################################

# PID file of the main process is stored here.  Backend can read this file to
# detect the last PID of copr-builder run.
opt_pidfile=/var/lib/copr-builder/pid

# Build results are stored here.
opt_resultdir=/var/lib/copr-builder/results

# This is to be read by 'tail -n --retry +0 -f --pid=...' on backend side.
opt_log_file=/var/lib/copr-builder/live-log

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

opt_copr=
opt_package=
opt_revision=
opt_chroot=fedora-rawhide-x86_64
opt_config=/etc/copr-builder/fedora-copr.conf
opt_lockfile=/var/lib/copr-builder/lock


opt_workdir=
opt_timeout=
opt_workdir_cleanup=false
opt_download_mock_config=:
opt_debug=false
opt_host_resolv=
opt_detached=false


error() { echo >&2 " ! $*"; }
die()   { error "$*"; exit 1; }
info()  { echo 2>&1 " * $*"; }
debug() { if $opt_debug; then echo 2>&1 " ~ $*"; fi ; }

destruct ()
{
    debug "running script cleanup"
    if $opt_workdir_cleanup && test -n "$opt_workdir" && test -d "$opt_workdir"
    then
        debug "cleaning workdir"
        rm -rf "$opt_workdir"
    fi

    if test -n "$opt_lockfile" && test -f "$opt_lockfile"
    then
        rm -rf "$opt_lockfile"
    fi
}

quote_args ()
{
    quote_args_result=
    local sp=
    for arg
    do
        quote_args_result+=$sp$(printf %q "$arg")
        sp=' '
    done
}

filter_backpaces ()
{
    # Drop terminal sequences and strings terminated by CR (not CR LF)
    sed -e 's|\x1B\[[0-9;]*[a-zA-Z]||g' \
        -e 's/.*\x0D\([^\x0a]\)/\1/g' --unbuffered
}

stdout_wrap ()
(
    set -o pipefail

    # Normally, to have both stdout and stderr cleaned from backspaces, we would
    # do '"$@" 2> >(filter_backpaces >&2) | filter_backpaces -b';  IOW attaching
    # fifo to stderr and piping stdout through col -b.  But because we want to
    # wrap some 'mock' calls too, and mock has the ugly hack with
    # 'stderr.isatty()' (otherwise the output is very quiet, rhbz#1166609), we
    # need to run through 'unbuffer' to fake the terminal.
    unbuffer "$@" 2> >(filter_backpaces >&2) | filter_backpaces
)

show_help()
{
cat <<EOHELP >&2
Usage: $0 OPTION

Build package against copr from copr dist-git.

Options:
  --copr                     copr; <user>/<project> or @<group>/<project>)
  --package                  component (package name) in particular copr
  --revision                 git commit reference in dist-git; git hash, tag,
                             branch..
  --chroot                   chroot, e.g. fedora-rawhide-x86_64
  --config                   configuration file
  --resultdir                directory to place build results
  --workdir                  by default, temporary working directory under
                             /var/lib/copr-builder is used
  --define="ARG VALUE"       define rpm macro
  --host-resolv=[0|1]        use host's resolv conf

  --detached                 fork the builder and run in background, this
                             always succeeds and prints PID of the child to
                             standard output
  --log-file=LOG             store stdout/stderr into LOG
  --timeout=SECONDS          fail miserably after SECONDS timeout
  --mock-opts=OPTS           additional mock options, shell-quote OPTS properly
EOHELP

test -n "$1" && exit "$1"
}

long_options=copr:,package:,revision:,config:,resultdir:,workdir:,define:
long_options+=,host-resolv:,detached,log-file:,timeout:,chroot:,mock-opts:
oldargs=( "$@" )
ARGS=$(getopt -o "h" -l "$long_options,help" -n "getopt" -- "$@")
mock=(mock)
eval set -- "$ARGS"

while :; do
    case "$1" in
    --copr|--package|--revision|--config|--resultdir|--workdir| \
    --host-resolv|--log-file|--timeout|--chroot)
        opt=${1##--}
        opt=${opt##-}
        opt=${opt//-/_}
        eval "opt_$opt=\$2"
        shift 2
        ;;

    --detached)
        opt=${1##--}
        opt=${opt##-}
        opt=${opt//-/_}
        eval "opt_$opt=:"
        shift 1
        ;;

    --mock-opts)
        # Eval is needed to keep the right number of arguments, e.g.
        # --mock-opts='--rpmbuild-opts "-vv --fsmdebug -ddd --rpmiodebug"'
        eval "mock+=( $2 )"
        shift 2
        ;;

    --define)
        mock+=(--define "$2")
        shift 2
        ;;

    --help)
        show_help 0
        ;;

    --) # end!
        shift
        break
        ;;

    *)
        echo "programmer mistake ($1)" >&2
        exit 1
        ;;
    esac
done

if $opt_detached; then
    # Parent process.  Re-exec ourselves in background, but now  _without_
    # the --detached option.
    new_args=()
    for arg in "${oldargs[@]}"
    do
        case $arg in
        --detached) ;;
        *) new_args+=( "$arg" ) ;;
        esac
    done
    # Copr backend runs:
    #   'ssh -t <opts> "copr-builder --detached"'
    # This triggers those processes on builder-side:
    #   (0) + bash -c 'copr-builder --detached'
    #   (1)   + copr-builder --detached
    #   (2)     + copr-builder ... (detached)
    # The pseudo terminal allocated for this session is closed right after the
    # 'exit 0' command below, which means SIGHUP is sent to (2) command.
    # Ignoring the SIGHUP in (2) would be racy (signal can be delivered before
    # the signal handler is actually installed) so we rather ignore it here
    # already in process (1).
    trap "" SIGHUP
    "$0" "${new_args[@]}" &>/dev/null &
    echo $!
    exit 0
fi

# Reexec ourselves if we want to timeout.
test -n "$opt_timeout" && test -z "$TIMEOUT_SET" \
    && TIMEOUT_SET=: exec timeout "$opt_timeout" "$0" "${oldargs[@]}"

# Start doing the work, but ensure only one copr-builder at the same time!
set -e
exec 9>"$opt_lockfile"
flock -n 9 || die "can't lock $opt_lockfile"

# Ensure there are no leftovers.
trap destruct EXIT

# Make the "re-attaching" in copr-backend Worker convenient.  That's to be done
# by 'tail --retry -f --pid=`cat pidfile`'.
echo $$ > "$opt_pidfile"

# Duplicate all output to the log file.
exec &> >(tee -i "$opt_log_file")

quote_args copr-builder "${oldargs[@]}"
info "running command: $quote_args_result"

opt_parse_error ()
{
    error "$*"
    opt_parse_success=false
}

opt_parse_success=:
for opt in copr package revision config chroot; do
    eval "test -z \"\$opt_$opt\"" \
        && opt_parse_error "missing argument --$opt"
done

test -r "$opt_config" \
    || opt_parse_error "Missing or unreadable config file '$opt_config'"
opt_config=$(readlink -f "$opt_config")

case $opt_resultdir in
:tmp:)
    opt_resultdir=$(mktemp -d)
    info "Results to be placed into '$opt_resultdir'"
    ;;
/var/lib/copr-builder/results)
    # We know that it is safe to remove everything from here.
    rm -rf "$opt_resultdir"
    mkdir "$opt_resultdir"
    ;;
esac

test -d "$opt_resultdir" || opt_parse_error "'$opt_resultdir' is not dir"

if test -n "$opt_workdir"; then
    if test -d "$opt_workdir"; then
        test -w "$opt_workdir" || opt_parse_error "'$opt_workdir' is not a dir"
    else
        opt_parse_error "Directory '$opt_workdir' is not writeable."
    fi
else
    opt_workdir_cleanup=:
    opt_workdir=$(mktemp -d /var/lib/copr-builder/build-XXXXX) \
        || opt_parse_error "can't create workdir"
fi

case $opt_host_resolv in
    1|yes|True|true)   opt_host_resolv=True  ;;
    0|no|False|false)  opt_host_resolv=False ;;
    '') ;; # default: unchanged
    *) opt_parse_error "Use --host-resolv=1 (or 0)" ;;
esac

$opt_parse_success

eval "$(crudini --format=sh --get "$opt_config" copr_builder)"

success=:
for opt in branching
do
    eval "test -z \"\$$opt\"" \
        && error "missing config option $opt" \
        && success=false
done
$success

# Construct 'rpkg' options, use them as '$@' later.
set -- --verbose --debug --config "$opt_config"

# TODO: drop this hack after https://pagure.io/rpkg/pull-request/212
set -- "$@" --release rhel-7.2

cd "$opt_workdir"

info "Copying system mock configuration"
configs_dir=$opt_resultdir/configs
mkdir -p "$configs_dir"
cp /etc/mock/site-defaults.cfg "$configs_dir"
cp "/etc/mock/$opt_chroot.cfg" "$configs_dir"

info "Downloading changed mock config from frontend"
if $opt_download_mock_config; then
    copr --config "$opt_config" mock-config "$opt_copr" "$opt_chroot" \
        > "$configs_dir"/changed.cfg
fi
mock+=(--configdir "$configs_dir" -r changed)

if test -n "$opt_host_resolv"; then
   echo "config_opts['use_host_resolv'] = $opt_host_resolv" \
       >> "$configs_dir"/changed.cfg
fi

info "Obtain sources from dist-git"
stdout_wrap rpkg "$@" clone -a "$opt_copr/$opt_package" pkg-git
(
cd pkg-git
git checkout "$opt_revision"
stdout_wrap rpkg "$@" --module-name "$opt_copr/$opt_package" sources
)

info "Generate SRPM in mock $opt_chroot"
stdout_wrap "${mock[@]}" \
    --buildsrpm \
    --spec pkg-git/"$opt_package".spec \
    --sources pkg-git \
    --resultdir intermediate-srpm \
    --no-cleanup-after

info "Generate RPM in cached mock chroot"
stdout_wrap "${mock[@]}" \
    --rebuild intermediate-srpm/"$opt_package"*.src.rpm \
    --resultdir "$opt_resultdir" \
    --no-clean

echo done > "$opt_resultdir"/success