542 lines
16 KiB
Bash
Executable file
542 lines
16 KiB
Bash
Executable file
#!/bin/bash
|
|
# Simple shell script for driving a remote cowbuilder via ssh
|
|
#
|
|
# Copyright(C) 2007, 2008, 2009, 2011, 2012, 2014, Ron <ron@debian.org>
|
|
# This script is distributed according to the terms of the GNU GPL.
|
|
|
|
set -e
|
|
|
|
#BUILDD_HOST=
|
|
#BUILDD_USER=
|
|
BUILDD_ARCH="$(dpkg-architecture -qDEB_BUILD_ARCH 2>/dev/null)"
|
|
|
|
# The 'default' dist is whatever cowbuilder is locally configured for
|
|
BUILDD_DIST="default"
|
|
|
|
INCOMING_DIR="cowbuilder-incoming"
|
|
PBUILDER_BASE="/var/cache/pbuilder"
|
|
|
|
#SIGN_KEYID=
|
|
#UPLOAD_QUEUE="ftp-master"
|
|
BUILDD_ROOTCMD="sudo"
|
|
|
|
REMOTE_SCRIPT="cowssh_it"
|
|
DEBOOTSTRAP="cdebootstrap"
|
|
|
|
for f in /etc/cowpoke.conf ~/.cowpoke .cowpoke "$COWPOKE_CONF"; do [ -r "$f" ] && . "$f"; done
|
|
|
|
|
|
get_archdist_vars() {
|
|
_ARCHDIST_OPTIONS="RESULT_DIR BASE_PATH BASE_DIST CREATE_OPTS UPDATE_OPTS BUILD_OPTS SIGN_KEYID UPLOAD_QUEUE"
|
|
_RESULT_DIR="result"
|
|
_BASE_PATH="base.cow"
|
|
|
|
for arch in $BUILDD_ARCH; do
|
|
for dist in $BUILDD_DIST; do
|
|
for var in $_ARCHDIST_OPTIONS; do
|
|
eval "val=( \"\${${arch}_${dist}_${var}[@]}\" )"
|
|
|
|
if [ "$1" = "display" ]; then
|
|
case $var in
|
|
RESULT_DIR | BASE_PATH )
|
|
[ ${#val[@]} -gt 0 ] || eval "val=\"$PBUILDER_BASE/$arch/$dist/\$_$var\""
|
|
echo " ${arch}_${dist}_${var} = $val"
|
|
;;
|
|
|
|
*_OPTS )
|
|
# Don't display these if they are overridden on the command line.
|
|
eval "override=( \"\${OVERRIDE_${var}[@]}\" )"
|
|
[ ${#override[@]} -gt 0 ] || [ ${#val[@]} -eq 0 ] ||
|
|
echo " ${arch}_${dist}_${var} =$(printf " '%s'" "${val[@]}")"
|
|
;;
|
|
|
|
* )
|
|
[ ${#val[@]} -eq 0 ] || echo " ${arch}_${dist}_${var} = $val"
|
|
;;
|
|
esac
|
|
else
|
|
case $var in
|
|
RESULT_DIR | BASE_PATH )
|
|
# These are always a single value, and must always be set,
|
|
# either by the user or to their default value.
|
|
[ ${#val[@]} -gt 0 ] || eval "val=\"$PBUILDER_BASE/$arch/$dist/\$_$var\""
|
|
echo "${arch}_${dist}_${var}='$val'"
|
|
;;
|
|
|
|
*_OPTS )
|
|
# These may have zero, one, or many values which we must not word-split.
|
|
# They can safely remain unset if there are no values.
|
|
#
|
|
# We don't need to worry about the command line overrides here,
|
|
# they will be taken care of in the remote script.
|
|
[ ${#val[@]} -eq 0 ] ||
|
|
echo "${arch}_${dist}_${var}=($(printf " %q" "${val[@]}") )"
|
|
;;
|
|
|
|
SIGN_KEYID | UPLOAD_QUEUE )
|
|
# We don't need these in the remote script
|
|
;;
|
|
|
|
* )
|
|
# These may have zero or one value.
|
|
# They can safely remain unset if there are no values.
|
|
[ ${#val[@]} -eq 0 ] || echo "${arch}_${dist}_${var}='$val'"
|
|
;;
|
|
esac
|
|
fi
|
|
done
|
|
done
|
|
done
|
|
}
|
|
|
|
display_override_vars() {
|
|
_OVERRIDE_OPTIONS="CREATE_OPTS UPDATE_OPTS BUILD_OPTS SIGN_KEYID UPLOAD_QUEUE"
|
|
|
|
for var in $_OVERRIDE_OPTIONS; do
|
|
eval "override=( \"\${OVERRIDE_${var}[@]}\" )"
|
|
[ ${#override[@]} -eq 0 ] || echo " override: $var =$(printf " '%s'" "${override[@]}")"
|
|
done
|
|
}
|
|
|
|
|
|
PROGNAME=${0##*/}
|
|
version() {
|
|
echo \
|
|
"This is $PROGNAME, from the Debian devscripts package, version ###VERSION###
|
|
This code is Copyright 2007-2014, Ron <ron@debian.org>.
|
|
This program comes with ABSOLUTELY NO WARRANTY.
|
|
You are free to redistribute this code under the terms of the
|
|
GNU General Public License."
|
|
exit 0
|
|
}
|
|
|
|
usage() {
|
|
cat 1>&2 <<EOF
|
|
|
|
cowpoke [options] package.dsc
|
|
|
|
Uploads a Debian source package to a cowbuilder host and builds it,
|
|
optionally also signing and uploading the result to an incoming queue.
|
|
The following options are supported:
|
|
|
|
--arch="arch" Specify the Debian architecture(s) to build for.
|
|
--dist="dist" Specify the Debian distribution(s) to build for.
|
|
--buildd="host" Specify the remote host to build on.
|
|
--buildd-user="name" Specify the remote user to build as.
|
|
--create Create the remote cowbuilder root if necessary.
|
|
--return[="path"] Copy results of the build to 'path'. If path is
|
|
not specified, return them to the current directory.
|
|
--no-return Do not copy results of the build to RETURN_DIR
|
|
(overriding a path set for it in the config files).
|
|
--sign="keyid" Specify the key to sign packages with.
|
|
--upload="queue" Specify the dput queue to upload signed packages to.
|
|
|
|
The current default configuration is:
|
|
|
|
BUILDD_HOST = $BUILDD_HOST
|
|
BUILDD_USER = $BUILDD_USER
|
|
BUILDD_ARCH = $BUILDD_ARCH
|
|
BUILDD_DIST = $BUILDD_DIST
|
|
RETURN_DIR = $RETURN_DIR
|
|
SIGN_KEYID = $SIGN_KEYID
|
|
UPLOAD_QUEUE = $UPLOAD_QUEUE
|
|
|
|
The expected remote paths are:
|
|
|
|
INCOMING_DIR = $INCOMING_DIR
|
|
PBUILDER_BASE = ${PBUILDER_BASE:-/}
|
|
|
|
$(get_archdist_vars display)
|
|
$(display_override_vars)
|
|
|
|
The cowbuilder image must have already been created on the build host
|
|
and the expected remote paths must already exist if the --create option
|
|
is not passed. You must have ssh access to the build host as BUILDD_USER
|
|
if that is set, else as the user executing cowpoke or a user specified
|
|
in your ssh config for '$BUILDD_HOST'.
|
|
That user must be able to execute cowbuilder as root using '$BUILDD_ROOTCMD'.
|
|
|
|
EOF
|
|
|
|
exit $1
|
|
}
|
|
|
|
|
|
for arg; do
|
|
case "$arg" in
|
|
--arch=*)
|
|
BUILDD_ARCH="${arg#*=}"
|
|
;;
|
|
|
|
--dist=*)
|
|
BUILDD_DIST="${arg#*=}"
|
|
;;
|
|
|
|
--buildd=*)
|
|
BUILDD_HOST="${arg#*=}"
|
|
;;
|
|
|
|
--buildd-user=*)
|
|
BUILDD_USER="${arg#*=}"
|
|
;;
|
|
|
|
--create)
|
|
CREATE_COW="yes"
|
|
;;
|
|
|
|
--return=*)
|
|
RETURN_DIR="${arg#*=}"
|
|
;;
|
|
|
|
--return)
|
|
RETURN_DIR=.
|
|
;;
|
|
|
|
--no-return)
|
|
RETURN_DIR=
|
|
;;
|
|
|
|
--dpkg-opts=*)
|
|
# This one is a bit tricky, given the combination of the calling convention here,
|
|
# the calling convention for cowbuilder, and the behaviour of things that might
|
|
# pass this option to us. Some things, like when we are called from the gitpkg
|
|
# hook using options from git-config, will preserve any quoting that was used in
|
|
# the .gitconfig file, which is natural for anyone to want to use in a construct
|
|
# like: options = --dpkg-opts='-uc -us -j6'. People are going to cringe if we
|
|
# tell them they must not use quotes there no matter how much it may 'make sense'
|
|
# if you know too much about the internals. And it will only get worse when we
|
|
# then tell them they must quote it like that if they type it directly in their
|
|
# shell ...
|
|
#
|
|
# So we do the only thing that seems sensible, and try to Deal With It here.
|
|
# If the outermost characters are paired quotes, we manually strip them off.
|
|
# We don't want to let the shell do quote removal, since that might change a
|
|
# part of this which we don't want modified.
|
|
# We collect however many sets of those we are passed in an array, which we'll
|
|
# then combine back into a single argument at the final point of use.
|
|
#
|
|
# Which _should_ DTRT for anyone who isn't trying to blow this up deliberately
|
|
# and maybe will still do it for them too in spite of their efforts. But unless
|
|
# someone finds a sensible case this fails on, I'm not going to cry over people
|
|
# who want to stuff up their own system with input they created themselves.
|
|
val=${arg#*=}
|
|
[[ $val == \'*\' || $val == \"*\" ]] && val=${val:1:-1}
|
|
DEBBUILDOPTS+=( "$val" )
|
|
;;
|
|
|
|
--create-opts=*)
|
|
OVERRIDE_CREATE_OPTS+=( "${arg#*=}" )
|
|
;;
|
|
|
|
--update-opts=*)
|
|
OVERRIDE_UPDATE_OPTS+=( "${arg#*=}" )
|
|
;;
|
|
|
|
--build-opts=*)
|
|
OVERRIDE_BUILD_OPTS+=( "${arg#*=}" )
|
|
;;
|
|
|
|
--sign=*)
|
|
OVERRIDE_SIGN_KEYID=${arg#*=}
|
|
;;
|
|
|
|
--upload=*)
|
|
OVERRIDE_UPLOAD_QUEUE=${arg#*=}
|
|
;;
|
|
|
|
*.dsc)
|
|
DSC="$arg"
|
|
;;
|
|
|
|
--help)
|
|
usage 0
|
|
;;
|
|
|
|
--version)
|
|
version
|
|
;;
|
|
|
|
*)
|
|
echo "ERROR: unrecognised option '$arg'"
|
|
usage 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [ -z "$REMOTE_SCRIPT" ]; then
|
|
echo "No remote script name set. Aborted."
|
|
exit 1
|
|
fi
|
|
if [ -z "$DSC" ]; then
|
|
echo "ERROR: No package .dsc specified"
|
|
usage 1
|
|
fi
|
|
if ! [ -r "$DSC" ]; then
|
|
echo "ERROR: '$DSC' not found."
|
|
exit 1
|
|
fi
|
|
if [ -z "$BUILDD_ARCH" ]; then
|
|
echo "No BUILDD_ARCH set. Aborted."
|
|
exit 1
|
|
fi
|
|
if [ -z "$BUILDD_HOST" ]; then
|
|
echo "No BUILDD_HOST set. Aborted."
|
|
exit 1
|
|
fi
|
|
if [ -z "$BUILDD_ROOTCMD" ]; then
|
|
echo "No BUILDD_ROOTCMD set. Aborted."
|
|
exit 1
|
|
fi
|
|
if [ -e "$REMOTE_SCRIPT" ]; then
|
|
echo "$REMOTE_SCRIPT file already exists and will be overwritten."
|
|
echo -n "Do you wish to continue (Y/n)? "
|
|
read -e yesno
|
|
case "$yesno" in
|
|
N* | n*)
|
|
echo "Ok, bailing out."
|
|
echo "You should set the REMOTE_SCRIPT variable to some other value"
|
|
echo "if this name conflicts with something you already expect to use"
|
|
exit 1
|
|
;;
|
|
*) ;;
|
|
esac
|
|
fi
|
|
|
|
[ -z "$BUILDD_USER" ] || BUILDD_USER="$BUILDD_USER@"
|
|
|
|
PACKAGE="$(basename $DSC .dsc)"
|
|
DATE="$(date +%Y%m%d 2>/dev/null)"
|
|
|
|
|
|
cat > "$REMOTE_SCRIPT" <<-EOF
|
|
#!/bin/bash
|
|
# cowpoke generated remote worker script.
|
|
# Normally this should have been deleted already, you can safely remove it now.
|
|
|
|
compare_changes() {
|
|
p1="\${1%_*.changes}"
|
|
p2="\${2%_*.changes}"
|
|
p1="\${p1##*_}"
|
|
p2="\${p2##*_}"
|
|
|
|
dpkg --compare-versions "\$p1" gt "\$p2"
|
|
}
|
|
|
|
$(get_archdist_vars)
|
|
|
|
for arch in $BUILDD_ARCH; do
|
|
for dist in $BUILDD_DIST; do
|
|
|
|
echo " ------- Begin build for \$arch \$dist -------"
|
|
|
|
CHANGES="\$arch.changes"
|
|
LOGFILE="$INCOMING_DIR/build.${PACKAGE}_\$arch.\$dist.log"
|
|
UPDATELOG="$INCOMING_DIR/cowbuilder-\${arch}-\${dist}-update-log-$DATE"
|
|
eval "RESULT_DIR=\"\\\$\${arch}_\${dist}_RESULT_DIR\""
|
|
eval "BASE_PATH=\"\\\$\${arch}_\${dist}_BASE_PATH\""
|
|
eval "BASE_DIST=\"\\\$\${arch}_\${dist}_BASE_DIST\""
|
|
eval "CREATE_OPTS=( \"\\\${\${arch}_\${dist}_CREATE_OPTS[@]}\" )"
|
|
eval "UPDATE_OPTS=( \"\\\${\${arch}_\${dist}_UPDATE_OPTS[@]}\" )"
|
|
eval "BUILD_OPTS=( \"\\\${\${arch}_\${dist}_BUILD_OPTS[@]}\" )"
|
|
|
|
[ -n "\$BASE_DIST" ] || BASE_DIST=\$dist
|
|
[ ${#OVERRIDE_CREATE_OPTS[@]} -eq 0 ] || CREATE_OPTS=("${OVERRIDE_CREATE_OPTS[@]}")
|
|
[ ${#OVERRIDE_UPDATE_OPTS[@]} -eq 0 ] || UPDATE_OPTS=("${OVERRIDE_UPDATE_OPTS[@]}")
|
|
[ ${#OVERRIDE_BUILD_OPTS[@]} -eq 0 ] || BUILD_OPTS=("${OVERRIDE_BUILD_OPTS[@]}")
|
|
[ ${#DEBBUILDOPTS[*]} -eq 0 ] || DEBBUILDOPTS=("--debbuildopts" "${DEBBUILDOPTS[*]}")
|
|
|
|
|
|
# Sort the list of old changes files for this package to try and
|
|
# determine the most recent one preceding this version. We will
|
|
# debdiff to this revision in the final sanity checks if one exists.
|
|
# This is adapted from the insertion sort trickery in git-debimport.
|
|
|
|
OLD_CHANGES="\$(find "\$RESULT_DIR/" -maxdepth 1 -type f \\
|
|
-name "${PACKAGE%%_*}_*_\$CHANGES" 2>/dev/null \\
|
|
| sort 2>/dev/null)"
|
|
P=( \$OLD_CHANGES )
|
|
count=\${#P[*]}
|
|
|
|
for(( i=1; i < count; ++i )) do
|
|
j=i
|
|
#echo "was \$i: \${P[i]}"
|
|
while ((\$j)) && compare_changes "\${P[j-1]}" "\${P[i]}"; do ((--j)); done
|
|
((i==j)) || P=( \${P[@]:0:j} \${P[i]} \${P[j]} \${P[@]:j+1:i-(j+1)} \${P[@]:i+1} )
|
|
done
|
|
#for(( i=1; i < count; ++i )) do echo "now \$i: \${P[i]}"; done
|
|
|
|
OLD_CHANGES=
|
|
for(( i=count-1; i >= 0; --i )) do
|
|
if [ "\${P[i]}" != "\$RESULT_DIR/${PACKAGE}_\$CHANGES" ]; then
|
|
OLD_CHANGES="\${P[i]}"
|
|
break
|
|
fi
|
|
done
|
|
|
|
|
|
set -eo pipefail
|
|
|
|
if ! [ -e "\$BASE_PATH" ]; then
|
|
if [ "$CREATE_COW" = "yes" ]; then
|
|
mkdir -p "\$RESULT_DIR"
|
|
mkdir -p "\$(dirname \$BASE_PATH)"
|
|
mkdir -p "$PBUILDER_BASE/aptcache"
|
|
$BUILDD_ROOTCMD cowbuilder --create --distribution \$BASE_DIST \\
|
|
--basepath "\$BASE_PATH" \\
|
|
--aptcache "$PBUILDER_BASE/aptcache" \\
|
|
--debootstrap "$DEBOOTSTRAP" \\
|
|
--debootstrapopts --arch="\$arch" \\
|
|
"\${CREATE_OPTS[@]}" \\
|
|
2>&1 | tee "\$UPDATELOG"
|
|
else
|
|
echo "SKIPPING \$dist/\$arch build, '\$BASE_PATH' does not exist" | tee "\$LOGFILE"
|
|
echo " use the cowpoke --create option to bootstrap a new build root" | tee -a "\$LOGFILE"
|
|
continue
|
|
fi
|
|
elif ! [ -e "\$UPDATELOG" ]; then
|
|
$BUILDD_ROOTCMD cowbuilder --update --distribution \$BASE_DIST \\
|
|
--basepath "\$BASE_PATH" \\
|
|
--aptcache "$PBUILDER_BASE/aptcache" \\
|
|
--autocleanaptcache \\
|
|
"\${UPDATE_OPTS[@]}" \\
|
|
2>&1 | tee "\$UPDATELOG"
|
|
fi
|
|
$BUILDD_ROOTCMD cowbuilder --build --basepath "\$BASE_PATH" \\
|
|
--aptcache "$PBUILDER_BASE/aptcache" \\
|
|
--buildplace "$PBUILDER_BASE/build" \\
|
|
--buildresult "\$RESULT_DIR" \\
|
|
"\${DEBBUILDOPTS[@]}" \\
|
|
"\${BUILD_OPTS[@]}" \\
|
|
"$INCOMING_DIR/$(basename $DSC)" 2>&1 \\
|
|
| tee "\$LOGFILE"
|
|
|
|
set +eo pipefail
|
|
|
|
|
|
echo >> "\$LOGFILE"
|
|
echo "lintian \$RESULT_DIR/${PACKAGE}_\$CHANGES" >> "\$LOGFILE"
|
|
lintian "\$RESULT_DIR/${PACKAGE}_\$CHANGES" 2>&1 | tee -a "\$LOGFILE"
|
|
|
|
if [ -n "\$OLD_CHANGES" ]; then
|
|
echo >> "\$LOGFILE"
|
|
echo "debdiff \$OLD_CHANGES ${PACKAGE}_\$CHANGES" >> "\$LOGFILE"
|
|
debdiff "\$OLD_CHANGES" "\$RESULT_DIR/${PACKAGE}_\$CHANGES" 2>&1 \\
|
|
| tee -a "\$LOGFILE"
|
|
else
|
|
echo >> "\$LOGFILE"
|
|
echo "No previous packages for \$dist/\$arch to compare" >> "\$LOGFILE"
|
|
fi
|
|
|
|
done
|
|
done
|
|
|
|
EOF
|
|
chmod 755 "$REMOTE_SCRIPT"
|
|
|
|
|
|
if ! dcmd rsync -vP $DSC "$REMOTE_SCRIPT" "$BUILDD_USER$BUILDD_HOST:$INCOMING_DIR";
|
|
then
|
|
dcmd scp $DSC "$REMOTE_SCRIPT" "$BUILDD_USER$BUILDD_HOST:$INCOMING_DIR"
|
|
fi
|
|
|
|
ssh -t "$BUILDD_USER$BUILDD_HOST" "\"$INCOMING_DIR/$REMOTE_SCRIPT\" && rm -f \"$INCOMING_DIR/$REMOTE_SCRIPT\""
|
|
|
|
echo
|
|
echo "Build completed."
|
|
|
|
for arch in $BUILDD_ARCH; do
|
|
CHANGES="$arch.changes"
|
|
for dist in $BUILDD_DIST; do
|
|
|
|
sign_keyid=$OVERRIDE_SIGN_KEYID
|
|
[ -n "$sign_keyid" ] || eval "sign_keyid=\"\$${arch}_${dist}_SIGN_KEYID\""
|
|
[ -n "$sign_keyid" ] || sign_keyid="$SIGN_KEYID"
|
|
[ -n "$sign_keyid" ] || continue
|
|
|
|
eval "RESULT_DIR=\"\$${arch}_${dist}_RESULT_DIR\""
|
|
[ -n "$RESULT_DIR" ] || RESULT_DIR="$PBUILDER_BASE/$arch/$dist/result"
|
|
|
|
_desc="$dist/$arch"
|
|
[ "$dist" != "default" ] || _desc="$arch"
|
|
|
|
while true; do
|
|
echo -n "Sign $_desc $PACKAGE with key '$sign_keyid' (yes/no)? "
|
|
read -e yesno
|
|
case "$yesno" in
|
|
YES | yes)
|
|
debsign "-k$sign_keyid" -r "$BUILDD_USER$BUILDD_HOST" "$RESULT_DIR/${PACKAGE}_$CHANGES"
|
|
|
|
upload_queue=$OVERRIDE_UPLOAD_QUEUE
|
|
[ -n "$upload_queue" ] || eval "upload_queue=\"\$${arch}_${dist}_UPLOAD_QUEUE\""
|
|
[ -n "$upload_queue" ] || upload_queue="$UPLOAD_QUEUE"
|
|
|
|
if [ -n "$upload_queue" ]; then
|
|
while true; do
|
|
echo -n "Upload $_desc $PACKAGE to '$upload_queue' (yes/no)? "
|
|
read -e upload
|
|
case "$upload" in
|
|
YES | yes)
|
|
ssh "$BUILDD_USER$BUILDD_HOST" \
|
|
"cd \"$RESULT_DIR/\" && dput \"$upload_queue\" \"${PACKAGE}_$CHANGES\""
|
|
break 2
|
|
;;
|
|
|
|
NO | no)
|
|
echo "Package upload skipped."
|
|
break 2
|
|
;;
|
|
*)
|
|
echo "Please answer 'yes' or 'no'"
|
|
;;
|
|
esac
|
|
done
|
|
fi
|
|
break
|
|
;;
|
|
|
|
NO | no)
|
|
echo "Package signing skipped."
|
|
break
|
|
;;
|
|
*)
|
|
echo "Please answer 'yes' or 'no'"
|
|
;;
|
|
esac
|
|
done
|
|
done
|
|
done
|
|
|
|
if [ -n "$RETURN_DIR" ]; then
|
|
for arch in $BUILDD_ARCH; do
|
|
CHANGES="$arch.changes"
|
|
for dist in $BUILDD_DIST; do
|
|
|
|
eval "RESULT_DIR=\"\$${arch}_${dist}_RESULT_DIR\""
|
|
[ -n "$RESULT_DIR" ] || RESULT_DIR="$PBUILDER_BASE/$arch/$dist/result"
|
|
|
|
|
|
cache_dir="./cowpoke-return-cache"
|
|
mkdir -p $cache_dir
|
|
|
|
scp "$BUILDD_USER$BUILDD_HOST:$RESULT_DIR/${PACKAGE}_$CHANGES" $cache_dir
|
|
|
|
for f in $(cd $cache_dir && dcmd ${PACKAGE}_$CHANGES); do
|
|
RESULTS="$RESULTS $RESULT_DIR/$f"
|
|
done
|
|
|
|
rm -f $cache_dir/${PACKAGE}_$CHANGES
|
|
rmdir $cache_dir
|
|
|
|
|
|
if ! rsync -vP "$BUILDD_USER$BUILDD_HOST:$RESULTS" "$RETURN_DIR" ;
|
|
then
|
|
scp "$BUILDD_USER$BUILDD_HOST:$RESULTS" "$RETURN_DIR"
|
|
fi
|
|
|
|
done
|
|
done
|
|
fi
|
|
|
|
rm -f "$REMOTE_SCRIPT"
|
|
|
|
# vi:sts=4:sw=4:noet:foldmethod=marker
|