1#!/bin/bash
2# Simple shell script for driving a remote cowbuilder via ssh
3#
4# Copyright(C) 2007, 2008, 2009, 2011, 2012, 2014, Ron <ron@debian.org>
5# This script is distributed according to the terms of the GNU GPL.
6
7set -e
8
9#BUILDD_HOST=
10#BUILDD_USER=
11BUILDD_ARCH="$(dpkg-architecture -qDEB_BUILD_ARCH 2>/dev/null)"
12
13# The 'default' dist is whatever cowbuilder is locally configured for
14BUILDD_DIST="default"
15
16INCOMING_DIR="cowbuilder-incoming"
17PBUILDER_BASE="/var/cache/pbuilder"
18
19#SIGN_KEYID=
20#UPLOAD_QUEUE="ftp-master"
21BUILDD_ROOTCMD="sudo"
22
23REMOTE_SCRIPT="cowssh_it"
24DEBOOTSTRAP="cdebootstrap"
25
26for f in /etc/cowpoke.conf ~/.cowpoke .cowpoke "$COWPOKE_CONF"; do [ -r "$f" ] && . "$f"; done
27
28
29get_archdist_vars()
30{
31    _ARCHDIST_OPTIONS="RESULT_DIR BASE_PATH BASE_DIST CREATE_OPTS UPDATE_OPTS BUILD_OPTS SIGN_KEYID UPLOAD_QUEUE"
32    _RESULT_DIR="result"
33    _BASE_PATH="base.cow"
34
35    for arch in $BUILDD_ARCH; do
36	for dist in $BUILDD_DIST; do
37	    for var in $_ARCHDIST_OPTIONS; do
38		eval "val=( \"\${${arch}_${dist}_${var}[@]}\" )"
39
40		if [ "$1" = "display" ]; then
41		    case $var in
42			RESULT_DIR | BASE_PATH )
43			    [ ${#val[@]} -gt 0 ] || eval "val=\"$PBUILDER_BASE/$arch/$dist/\$_$var\""
44			    echo "   ${arch}_${dist}_${var} = $val"
45			    ;;
46
47			*_OPTS )
48			    # Don't display these if they are overridden on the command line.
49			    eval "override=( \"\${OVERRIDE_${var}[@]}\" )"
50			    [ ${#override[@]} -gt 0 ] || [ ${#val[@]} -eq 0 ] ||
51				echo "   ${arch}_${dist}_${var} =$(printf " '%s'" "${val[@]}")"
52			    ;;
53
54			* )
55			    [ ${#val[@]} -eq 0 ] || echo "   ${arch}_${dist}_${var} = $val"
56			    ;;
57		    esac
58		else
59		    case $var in
60			RESULT_DIR | BASE_PATH )
61			    # These are always a single value, and must always be set,
62			    # either by the user or to their default value.
63			    [ ${#val[@]} -gt 0 ] || eval "val=\"$PBUILDER_BASE/$arch/$dist/\$_$var\""
64			    echo "${arch}_${dist}_${var}='$val'"
65			    ;;
66
67			*_OPTS )
68			    # These may have zero, one, or many values which we must not word-split.
69			    # They can safely remain unset if there are no values.
70			    #
71			    # We don't need to worry about the command line overrides here,
72			    # they will be taken care of in the remote script.
73			    [ ${#val[@]} -eq 0 ] ||
74				echo "${arch}_${dist}_${var}=($(printf " %q" "${val[@]}") )"
75			    ;;
76
77			SIGN_KEYID | UPLOAD_QUEUE )
78			    # We don't need these in the remote script
79			    ;;
80
81			* )
82			    # These may have zero or one value.
83			    # They can safely remain unset if there are no values.
84			    [ ${#val[@]} -eq 0 ] || echo "${arch}_${dist}_${var}='$val'"
85			    ;;
86		    esac
87		fi
88	    done
89	done
90    done
91}
92
93display_override_vars()
94{
95    _OVERRIDE_OPTIONS="CREATE_OPTS UPDATE_OPTS BUILD_OPTS SIGN_KEYID UPLOAD_QUEUE"
96
97    for var in $_OVERRIDE_OPTIONS; do
98	eval "override=( \"\${OVERRIDE_${var}[@]}\" )"
99	[ ${#override[@]} -eq 0 ] || echo "   override: $var =$(printf " '%s'" "${override[@]}")"
100    done
101}
102
103
104PROGNAME="$(basename $0)"
105version ()
106{
107    echo \
108"This is $PROGNAME, from the Debian devscripts package, version ###VERSION###
109This code is Copyright 2007-2014, Ron <ron@debian.org>.
110This program comes with ABSOLUTELY NO WARRANTY.
111You are free to redistribute this code under the terms of the
112GNU General Public License."
113    exit 0
114}
115
116usage()
117{
118    cat 1>&2 <<EOF
119
120cowpoke [options] package.dsc
121
122  Uploads a Debian source package to a cowbuilder host and builds it,
123  optionally also signing and uploading the result to an incoming queue.
124  The following options are supported:
125
126   --arch="arch"         Specify the Debian architecture(s) to build for.
127   --dist="dist"         Specify the Debian distribution(s) to build for.
128   --buildd="host"       Specify the remote host to build on.
129   --buildd-user="name"  Specify the remote user to build as.
130   --create              Create the remote cowbuilder root if necessary.
131   --return[="path"]     Copy results of the build to 'path'.  If path is
132                         not specified, return them to the current directory.
133   --no-return           Do not copy results of the build to RETURN_DIR
134                         (overriding a path set for it in the config files).
135   --sign="keyid"        Specify the key to sign packages with.
136   --upload="queue"      Specify the dput queue to upload signed packages to.
137
138  The current default configuration is:
139
140   BUILDD_HOST   = $BUILDD_HOST
141   BUILDD_USER   = $BUILDD_USER
142   BUILDD_ARCH   = $BUILDD_ARCH
143   BUILDD_DIST   = $BUILDD_DIST
144   RETURN_DIR    = $RETURN_DIR
145   SIGN_KEYID    = $SIGN_KEYID
146   UPLOAD_QUEUE  = $UPLOAD_QUEUE
147
148  The expected remote paths are:
149
150   INCOMING_DIR  = $INCOMING_DIR
151   PBUILDER_BASE = ${PBUILDER_BASE:-/}
152
153$(get_archdist_vars display)
154$(display_override_vars)
155
156  The cowbuilder image must have already been created on the build host
157  and the expected remote paths must already exist if the --create option
158  is not passed.  You must have ssh access to the build host as BUILDD_USER
159  if that is set, else as the user executing cowpoke or a user specified
160  in your ssh config for '$BUILDD_HOST'.
161  That user must be able to execute cowbuilder as root using '$BUILDD_ROOTCMD'.
162
163EOF
164
165    exit $1
166}
167
168
169for arg; do
170    case "$arg" in
171	--arch=*)
172	    BUILDD_ARCH="${arg#*=}"
173	    ;;
174
175	--dist=*)
176	    BUILDD_DIST="${arg#*=}"
177	    ;;
178
179	--buildd=*)
180	    BUILDD_HOST="${arg#*=}"
181	    ;;
182
183	--buildd-user=*)
184	    BUILDD_USER="${arg#*=}"
185	    ;;
186
187	--create)
188	    CREATE_COW="yes"
189	    ;;
190
191	--return=*)
192	    RETURN_DIR="${arg#*=}"
193	    ;;
194
195	--return)
196	    RETURN_DIR=.
197	    ;;
198
199	--no-return)
200	    RETURN_DIR=
201	    ;;
202
203	--dpkg-opts=*)
204	    # This one is a bit tricky, given the combination of the calling convention here,
205	    # the calling convention for cowbuilder, and the behaviour of things that might
206	    # pass this option to us.  Some things, like when we are called from the gitpkg
207	    # hook using options from git-config, will preserve any quoting that was used in
208	    # the .gitconfig file, which is natural for anyone to want to use in a construct
209	    # like: options = --dpkg-opts='-uc -us -j6'.  People are going to cringe if we
210	    # tell them they must not use quotes there no matter how much it may 'make sense'
211	    # if you know too much about the internals.  And it will only get worse when we
212	    # then tell them they must quote it like that if they type it directly in their
213	    # shell ...
214	    #
215	    # So we do the only thing that seems sensible, and try to Deal With It here.
216	    # If the outermost characters are paired quotes, we manually strip them off.
217	    # We don't want to let the shell do quote removal, since that might change a
218	    # part of this which we don't want modified.
219	    # We collect however many sets of those we are passed in an array, which we'll
220	    # then combine back into a single argument at the final point of use.
221	    #
222	    # Which _should_ DTRT for anyone who isn't trying to blow this up deliberately
223	    # and maybe will still do it for them too in spite of their efforts. But unless
224	    # someone finds a sensible case this fails on, I'm not going to cry over people
225	    # who want to stuff up their own system with input they created themselves.
226	    val=${arg#*=}
227	    [[ $val == \'*\' || $val == \"*\" ]] && val=${val:1:-1}
228	    DEBBUILDOPTS+=( "$val" )
229	    ;;
230
231	--create-opts=*)
232	    OVERRIDE_CREATE_OPTS+=( "${arg#*=}" )
233	    ;;
234
235	--update-opts=*)
236	    OVERRIDE_UPDATE_OPTS+=( "${arg#*=}" )
237	    ;;
238
239	--build-opts=*)
240	    OVERRIDE_BUILD_OPTS+=( "${arg#*=}" )
241	    ;;
242
243	--sign=*)
244	    OVERRIDE_SIGN_KEYID=${arg#*=}
245	    ;;
246
247	--upload=*)
248	    OVERRIDE_UPLOAD_QUEUE=${arg#*=}
249	    ;;
250
251	*.dsc)
252	    DSC="$arg"
253	    ;;
254
255	--help)
256	    usage 0
257	    ;;
258
259	--version)
260	    version
261	    ;;
262
263	*)
264	    echo "ERROR: unrecognised option '$arg'"
265	    usage 1
266	    ;;
267    esac
268done
269
270if [ -z "$REMOTE_SCRIPT" ]; then
271    echo "No remote script name set.  Aborted."
272    exit 1
273fi
274if [ -z "$DSC" ]; then
275    echo "ERROR: No package .dsc specified"
276    usage 1
277fi
278if ! [ -r "$DSC" ]; then
279    echo "ERROR: '$DSC' not found."
280    exit 1
281fi
282if [ -z "$BUILDD_ARCH" ]; then
283    echo "No BUILDD_ARCH set.  Aborted."
284    exit 1
285fi
286if [ -z "$BUILDD_HOST" ]; then
287    echo "No BUILDD_HOST set.  Aborted."
288    exit 1
289fi
290if [ -z "$BUILDD_ROOTCMD" ]; then
291    echo "No BUILDD_ROOTCMD set.  Aborted."
292    exit 1
293fi
294if [ -e "$REMOTE_SCRIPT" ]; then
295    echo "$REMOTE_SCRIPT file already exists and will be overwritten."
296    echo -n "Do you wish to continue (Y/n)? "
297    read -e yesno
298    case "$yesno" in
299	N* | n*)
300	    echo "Ok, bailing out."
301	    echo "You should set the REMOTE_SCRIPT variable to some other value"
302	    echo "if this name conflicts with something you already expect to use"
303	    exit 1
304	    ;;
305	*) ;;
306    esac
307fi
308
309[ -z "$BUILDD_USER" ] || BUILDD_USER="$BUILDD_USER@"
310
311PACKAGE="$(basename $DSC .dsc)"
312DATE="$(date +%Y%m%d 2>/dev/null)"
313
314
315cat > "$REMOTE_SCRIPT" <<-EOF
316	#!/bin/bash
317	# cowpoke generated remote worker script.
318	# Normally this should have been deleted already, you can safely remove it now.
319
320	compare_changes()
321	{
322	    p1="\${1%_*.changes}"
323	    p2="\${2%_*.changes}"
324	    p1="\${p1##*_}"
325	    p2="\${p2##*_}"
326
327	    dpkg --compare-versions "\$p1" gt "\$p2"
328	}
329
330	$(get_archdist_vars)
331
332	for arch in $BUILDD_ARCH; do
333	  for dist in $BUILDD_DIST; do
334
335	    echo " ------- Begin build for \$arch \$dist -------"
336
337	    CHANGES="\$arch.changes"
338	    LOGFILE="$INCOMING_DIR/build.${PACKAGE}_\$arch.\$dist.log"
339	    UPDATELOG="$INCOMING_DIR/cowbuilder-\${arch}-\${dist}-update-log-$DATE"
340	    eval "RESULT_DIR=\"\\\$\${arch}_\${dist}_RESULT_DIR\""
341	    eval "BASE_PATH=\"\\\$\${arch}_\${dist}_BASE_PATH\""
342	    eval "BASE_DIST=\"\\\$\${arch}_\${dist}_BASE_DIST\""
343	    eval "CREATE_OPTS=( \"\\\${\${arch}_\${dist}_CREATE_OPTS[@]}\" )"
344	    eval "UPDATE_OPTS=( \"\\\${\${arch}_\${dist}_UPDATE_OPTS[@]}\" )"
345	    eval "BUILD_OPTS=( \"\\\${\${arch}_\${dist}_BUILD_OPTS[@]}\" )"
346
347	    [ -n "\$BASE_DIST" ]                  || BASE_DIST=\$dist
348	    [ ${#OVERRIDE_CREATE_OPTS[@]} -eq 0 ] || CREATE_OPTS=("${OVERRIDE_CREATE_OPTS[@]}")
349	    [ ${#OVERRIDE_UPDATE_OPTS[@]} -eq 0 ] || UPDATE_OPTS=("${OVERRIDE_UPDATE_OPTS[@]}")
350	    [ ${#OVERRIDE_BUILD_OPTS[@]}  -eq 0 ] || BUILD_OPTS=("${OVERRIDE_BUILD_OPTS[@]}")
351	    [ ${#DEBBUILDOPTS[*]} -eq 0 ]         || DEBBUILDOPTS=("--debbuildopts" "${DEBBUILDOPTS[*]}")
352
353
354	    # Sort the list of old changes files for this package to try and
355	    # determine the most recent one preceding this version.  We will
356	    # debdiff to this revision in the final sanity checks if one exists.
357	    # This is adapted from the insertion sort trickery in git-debimport.
358
359	    OLD_CHANGES="\$(find "\$RESULT_DIR/" -maxdepth 1 -type f \\
360	                         -name "${PACKAGE%%_*}_*_\$CHANGES" 2>/dev/null \\
361	                    | sort 2>/dev/null)"
362	    P=( \$OLD_CHANGES )
363	    count=\${#P[*]}
364
365	    for(( i=1; i < count; ++i )) do
366	        j=i
367	        #echo "was \$i: \${P[i]}"
368	        while ((\$j)) && compare_changes "\${P[j-1]}" "\${P[i]}"; do ((--j)); done
369	        ((i==j)) || P=( \${P[@]:0:j} \${P[i]} \${P[j]} \${P[@]:j+1:i-(j+1)} \${P[@]:i+1} )
370	    done
371	    #for(( i=1; i < count; ++i )) do echo "now \$i: \${P[i]}"; done
372
373	    OLD_CHANGES=
374	    for(( i=count-1; i >= 0; --i )) do
375	        if [ "\${P[i]}" != "\$RESULT_DIR/${PACKAGE}_\$CHANGES" ]; then
376	            OLD_CHANGES="\${P[i]}"
377	            break
378	        fi
379	    done
380
381
382	    set -eo pipefail
383
384	    if ! [ -e "\$BASE_PATH" ]; then
385	        if [ "$CREATE_COW" = "yes" ]; then
386	            mkdir -p "\$RESULT_DIR"
387	            mkdir -p "\$(dirname \$BASE_PATH)"
388	            mkdir -p "$PBUILDER_BASE/aptcache"
389	            $BUILDD_ROOTCMD cowbuilder --create --distribution \$BASE_DIST  \\
390	                                       --basepath "\$BASE_PATH"             \\
391	                                       --aptcache "$PBUILDER_BASE/aptcache" \\
392	                                       --debootstrap "$DEBOOTSTRAP"         \\
393	                                       --debootstrapopts --arch="\$arch"    \\
394	                                       "\${CREATE_OPTS[@]}"                 \\
395	            2>&1 | tee "\$UPDATELOG"
396	        else
397	            echo "SKIPPING \$dist/\$arch build, '\$BASE_PATH' does not exist" | tee "\$LOGFILE"
398	            echo "         use the cowpoke --create option to bootstrap a new build root" | tee -a "\$LOGFILE"
399	            continue
400	        fi
401	    elif ! [ -e "\$UPDATELOG" ]; then
402	        $BUILDD_ROOTCMD cowbuilder --update --distribution \$BASE_DIST  \\
403	                                   --basepath "\$BASE_PATH"             \\
404	                                   --aptcache "$PBUILDER_BASE/aptcache" \\
405	                                   --autocleanaptcache                  \\
406	                                   "\${UPDATE_OPTS[@]}"                 \\
407	        2>&1 | tee "\$UPDATELOG"
408	    fi
409	    $BUILDD_ROOTCMD cowbuilder --build --basepath "\$BASE_PATH"      \\
410	                               --aptcache "$PBUILDER_BASE/aptcache"  \\
411	                               --buildplace "$PBUILDER_BASE/build"   \\
412	                               --buildresult "\$RESULT_DIR"          \\
413	                               "\${DEBBUILDOPTS[@]}"                 \\
414	                               "\${BUILD_OPTS[@]}"                   \\
415	                               "$INCOMING_DIR/$(basename $DSC)" 2>&1 \\
416	    | tee "\$LOGFILE"
417
418	    set +eo pipefail
419
420
421	    echo >> "\$LOGFILE"
422	    echo "lintian \$RESULT_DIR/${PACKAGE}_\$CHANGES" >> "\$LOGFILE"
423	    lintian "\$RESULT_DIR/${PACKAGE}_\$CHANGES" 2>&1 | tee -a "\$LOGFILE"
424
425	    if [ -n "\$OLD_CHANGES" ]; then
426	        echo >> "\$LOGFILE"
427	        echo "debdiff \$OLD_CHANGES ${PACKAGE}_\$CHANGES" >> "\$LOGFILE"
428	        debdiff "\$OLD_CHANGES" "\$RESULT_DIR/${PACKAGE}_\$CHANGES" 2>&1 \\
429	        | tee -a "\$LOGFILE"
430	    else
431	        echo >> "\$LOGFILE"
432	        echo "No previous packages for \$dist/\$arch to compare" >> "\$LOGFILE"
433	    fi
434
435	  done
436	done
437
438EOF
439chmod 755 "$REMOTE_SCRIPT"
440
441
442if ! dcmd rsync -vP $DSC "$REMOTE_SCRIPT" "$BUILDD_USER$BUILDD_HOST:$INCOMING_DIR";
443then
444    dcmd scp $DSC "$REMOTE_SCRIPT" "$BUILDD_USER$BUILDD_HOST:$INCOMING_DIR"
445fi
446
447ssh -t "$BUILDD_USER$BUILDD_HOST" "\"$INCOMING_DIR/$REMOTE_SCRIPT\" && rm -f \"$INCOMING_DIR/$REMOTE_SCRIPT\""
448
449echo
450echo "Build completed."
451
452for arch in $BUILDD_ARCH; do
453    CHANGES="$arch.changes"
454    for dist in $BUILDD_DIST; do
455
456	sign_keyid=$OVERRIDE_SIGN_KEYID
457	[ -n "$sign_keyid" ] || eval "sign_keyid=\"\$${arch}_${dist}_SIGN_KEYID\""
458	[ -n "$sign_keyid" ] || sign_keyid="$SIGN_KEYID"
459	[ -n "$sign_keyid" ] || continue
460
461	eval "RESULT_DIR=\"\$${arch}_${dist}_RESULT_DIR\""
462	[ -n "$RESULT_DIR" ] || RESULT_DIR="$PBUILDER_BASE/$arch/$dist/result"
463
464	_desc="$dist/$arch"
465	[ "$dist" != "default" ] || _desc="$arch"
466
467	while true; do
468	    echo -n "Sign $_desc $PACKAGE with key '$sign_keyid' (yes/no)? "
469	    read -e yesno
470	    case "$yesno" in
471		YES | yes)
472		    debsign "-k$sign_keyid" -r "$BUILDD_USER$BUILDD_HOST" "$RESULT_DIR/${PACKAGE}_$CHANGES"
473
474		    upload_queue=$OVERRIDE_UPLOAD_QUEUE
475		    [ -n "$upload_queue" ] || eval "upload_queue=\"\$${arch}_${dist}_UPLOAD_QUEUE\""
476		    [ -n "$upload_queue" ] || upload_queue="$UPLOAD_QUEUE"
477
478		    if [ -n "$upload_queue" ]; then
479			while true; do
480			    echo -n "Upload $_desc $PACKAGE to '$upload_queue' (yes/no)? "
481			    read -e upload
482			    case "$upload" in
483				YES | yes)
484				    ssh "$BUILDD_USER$BUILDD_HOST" \
485					"cd \"$RESULT_DIR/\" && dput \"$upload_queue\" \"${PACKAGE}_$CHANGES\""
486				    break 2
487				    ;;
488
489				NO | no)
490				    echo "Package upload skipped."
491				    break 2
492				    ;;
493				*)
494				    echo "Please answer 'yes' or 'no'"
495				    ;;
496			    esac
497			done
498		    fi
499		    break
500		    ;;
501
502		NO | no)
503		    echo "Package signing skipped."
504		    break
505		    ;;
506		*)
507		    echo "Please answer 'yes' or 'no'"
508		    ;;
509	    esac
510	done
511    done
512done
513
514if [ -n "$RETURN_DIR" ]; then
515    for arch in $BUILDD_ARCH; do
516      CHANGES="$arch.changes"
517      for dist in $BUILDD_DIST; do
518
519	eval "RESULT_DIR=\"\$${arch}_${dist}_RESULT_DIR\""
520	[ -n "$RESULT_DIR" ] || RESULT_DIR="$PBUILDER_BASE/$arch/$dist/result"
521
522
523	cache_dir="./cowpoke-return-cache"
524	mkdir -p $cache_dir
525
526	scp "$BUILDD_USER$BUILDD_HOST:$RESULT_DIR/${PACKAGE}_$CHANGES" $cache_dir
527
528	for f in $(cd $cache_dir && dcmd ${PACKAGE}_$CHANGES); do
529	    RESULTS="$RESULTS $RESULT_DIR/$f"
530	done
531
532	rm -f $cache_dir/${PACKAGE}_$CHANGES
533	rmdir $cache_dir
534
535
536	if ! rsync -vP "$BUILDD_USER$BUILDD_HOST:$RESULTS" "$RETURN_DIR" ;
537	then
538	    scp "$BUILDD_USER$BUILDD_HOST:$RESULTS" "$RETURN_DIR"
539	fi
540
541      done
542    done
543fi
544
545rm -f "$REMOTE_SCRIPT"
546
547# vi:sts=4:sw=4:noet:foldmethod=marker
548