summaryrefslogtreecommitdiff
path: root/scripts/build-release
blob: 37082a8b2a50461d465a2d0413ca1271c3906ae8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
#!/bin/bash

## Copyright (C) 2018 Robert Krawitz
##
## 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, 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, see <https://www.gnu.org/licenses/>.
##
## Build release tarball

# Note that the shebang line is explicit here, not indirected through
# autoconf.  This allows the script to be run in a non-initialized workspace.
# We also require this script to be run in the root of a workspace so that
# it can find the script directory to get the version without having to be
# autotool-processed.

# 0) Ensure that we're in the root directory and have a sane environment

declare GIT=$(type -p git)
declare -a failed_modules

if [[ ! -s ChangeLog.pre-5.2.11 ]] ; then
    echo "$0 must be run from repository top level"
    exit 1
fi
declare ROOT=$(pwd)

STP_PARALLEL=${STP_PARALLEL:-$($ROOT/scripts/count-cpus)}

if [[ -z $GIT ]] ; then
    echo "Can't find git.  Cannot continue."
    exit 1
fi

declare MAKE=$(type -p make)

if [[ -z $MAKE ]] ; then
    echo "Can't find make.  Cannot continue."
    exit 1
fi

function pkg_version {
    $ROOT/scripts/gversion pkg
}

function pkg_tag {
    declare version=$(pkg_version)
    echo gutenprint-${version//./_}
}

# Clean up any trailing whitespace.

function preflight {
    local trailing_ws="$("$GIT" grep -Il '[	 ]$')"
    if [[ -n $trailing_ws ]] ; then
	console_log "The following files have trailing whitespace:"
	console_log "$trailing_ws"
	return 2
    fi
    return 0
}

# Git pre-checks (not version-specific)

function check_git {
    "$GIT" fetch

    # Check for uncommitted files.
    if [[ -n $("$GIT" status -uno --porcelain) ]] ; then
	console_log "*** ERROR: Uncommitted changes in repository:"
	"$GIT" status -uno | console_log
	return 2
    fi

    # Ensure that the workspace is up to date (git status -uno
    # --porcelain -b |grep -v ahead is empty -- it's OK to be ahead,
    # but not behind) and that we don't need to rebase (no merges.
    # Also check that we haven't diverged.

    ahead=$("$GIT" rev-list $("$GIT" rev-parse @{u})..@)
    behind=$("$GIT" rev-list $("$GIT" rev-parse @)..@{u})
    if [[ -n $ahead && -n $behind ]] ; then
	# Oops!  Both ahead *and* behind remote.  Really bad news!
	console_log "*** ERROR: HEAD and remote have diverged!"
	console_log "***        Please merge and rebase all changes!"
	return 2
    elif [[ -n $behind ]] ; then
	# We're behind.  Not good.
	console_log "*** ERROR: Behind remote by $(wc -w <<< $behind) commits."
	return 2
    elif [[ -n $ahead ]] ; then
	# We're ahead.  That's OK as long as there are no merge commits...
	merges=0
	for h in $ahead ; do
	    parents=$("$GIT" rev-parse $h^@ |wc -w)
	    (( $parents > 1 )) && merges=$((merges + 1))
	done
	console_log "*** Warning: Ahead of remote."
	if (( $merges > 0 )) ; then
	    (( $merges != 1 )) && pl=s
	    console_log "*** ERROR: $merges merge$pl between HEAD and remote"
	    return 2
	fi
    fi
    return 0
}

# Run autogen.sh to ensure that we're using default build settings
# Everything else depends on this.

function run_build {
    ./autogen.sh && make clean && make ${STP_PARALLEL:+-j$STP_PARALLEL} && return 0
    echo "FATAL error: preliminary build failed!"
    return 1
}

# Same as above, without make clean if we know we're in a clean
# environment (e. g. CI)
function run_build_fresh {
    ./autogen.sh && make ${STP_PARALLEL:+-j$STP_PARALLEL} && return 0
    echo "FATAL error: preliminary build failed!"
    return 1
}

# Git check tag.  This can't be run until after the build, because we
# don't have the version available until autogen.

function check_git_tag {
    # Make sure that the tag that we're going to want to apply isn't
    # already present.
    if [[ -n $("$GIT" show-ref "refs/tags/$(pkg_tag)") ]] ; then
	console_log "*** ERROR: Tag named $(pkg_tag) is already present"
	return 2
    fi
    return 0
}

# Check that we can build a clone of this workspace
function _check_git_builds {
    rev=$("$GIT" rev-parse @)
    cwd=$(pwd)
    cd $TESTREPO || return 1
    git clone $cwd .
    if [[ $? != 0 ]] ; then
	echo "Unable to clone repo"
	return 1
    fi
    git checkout "$rev" || return 1
    STP_LOG_NO_SUBDIR=1 STP_LOG_DIR=$STP_TEST_LOG_PREFIX scripts/build-release preflight run_build run_valgrind_minimal run_distcheck_minimal
}

function check_git_builds {
    export TESTREPO=$(mktemp -d)
    cwd=$(pwd)
    _check_git_builds
    status=$?
    cd $cwd
    [[ -d $TESTREPO ]] && rm -rf $TESTREPO
    return $status
}

# Run make valgrind-minimal.
#
#    This does a *very* limited set of valgrind checks, running
#    testpattern and rastertogutenprint on 9 (currently) selected
#    printers.  It takes about 30 seconds on my laptop.  Smoketest and
#    all.

function run_valgrind_minimal {
    make check-valgrind-minimal && return 0
    echo "make check-valgrind-minimal failed"
    return 2
}

function run_valgrind_fast {
    make check-valgrind-fast && return 0
    echo "make check-valgrind-fast failed"
    return 2
}

# Run make distcheck-fast.
#
#    This actually builds the tarball, unpacks the tarball, builds it
#    out of tree, runs a short set of tests against it, does a local
#    make install, followed by make uninstall, and makes sure no
#    debris is left around.  This runs configure with all default
#    arguments, so it is testing dynamically linked executables.
#
#    The particular tests it runs are:
#
#    - Conformance tests all non-translated non-simplified PPD files
#      and distinct global ones.
#
#    - Runs test-rastertogutenprint on distinct printers, with fast
#      options (minimum paper size, lowest resolution, very fast
#      dithering).
#
#    - Runs run-testpattern-2:
#
#      + Distinct printers, fast options
#
#      + Selected printers, with cross product of input mode (and bit
#        depth), color correction, ink type, and use gloss.
#
#    It also has the property of maybe updating the .po files.  These
#    will later need to be committed and included in the tag.  So we
#    have to do our check for uncommitted bits prior to this.
#
#    It has not escaped me that this could be part of a CI testing
#    process.  I don't know if Sourceforge has the necessary gittage
#    (as GitHub does) to allow a merge bot to run something like this
#    and only merge to the main repository if this suite passes.
#
#    The reason for the distcheck-fast is so that if something stupid
#    goes wrong it gets caught quickly.  It takes about 270 seconmds
#    on my laptop.  It would be Kind Of Annoying to spend hours
#    testing only to find out that something's not handling destdir
#    correctly or make clean isn't removing something.
#
#    Note that this can't be combined with valgrind, since this builds
#    dynamic executables which can't conveniently be valground since
#    they're actually shell scripts.
#
#    There's now an even faster check, distcheck-minimal, that only
#    tests a handful of printers.  It takes about 50 seconds to run.
#    But that's really most useful for testing the distcheck
#    apparatus.
#
# So far we're at just over 5 minutes on a Skylake Xeon E3-1505Mv5,
# which isn't too bad for a prerelease smoke test.  The rest of this
# takes a lot longer.

function run_distcheck_fast {
    make distcheck-fast && return 0
    echo "make distcheck-fast failed"
    return 2
}

# Run make check-valgrind
#
# This is slow.  It tests only unique printers, and a lot of extra
# combinations with a few printers, all using fast options.  It
# uses both CUPS and run-testpattern-2 testing.  However, it's
# essentially embarrassingly parallel.
#
# I'd like not to go too long without running it, as it's easy for
# things to make their way in.  For CI purposes, if we ever go
# there, like to find a happy medium.

function run_valgrind {
    make check-valgrind && return 0
    echo "make check-valgrind failed"
    return 2
}

# Run make check-full
#
#    This one I'm not sure of; do we need this or is this well enough
#    covered by the combination of distcheck-fast and check-valgrind?
#    It does take a while, but I haven't benchmarked it lately.
#
#    - Conformance test all PPD files
#
#    - Run test-rastertogutenprint on all printers, with default options
#
#    - Runs run-testpattern-2:
#
#      + Distinct printers, default options
#
#      + All printers, fast options
#
#      + Distinct printers, fast options, with cross product of input
#        mode (and bit depth), color correction, ink type, and use
#        gloss.
#
#    IIRC this takes 60-90 minutes on my laptop, but again, it
#    parallelizes very well.

function run_full {
    make check-full && return 0
    echo "make check-full failed"
    return 2
}

# Run make checksums-release to generate a new regression file.
#
# The problem here is what do we require for the release build.  Do
# we require a clean regression run (other than added
# printers/modes)?  There are legitimate reasons for changing, and
# having to rerun the procedure because the release engineer forgot a
# command line option is a bit harsh.  Something better might be to
# simply record changes unless there's an outright failure here, and
# let those be reviewed.
#
# For CI purposes, the default might be to require no changes, with
# human intervention if there are.
#
# This takes about 30 minutes on my laptop.  This is extremely
# scalable.  Give us a really big machine instance to run it on, this
# will run really fast.

function run_checksums() {
    if make checksums ; then
	CSUM_FILE="src/testpattern/Checksums/sums.$(pkg_version).zpaq"
	if [[ ! -f $CSUM_FILE ]] ; then
	    echo "Can't find new checksums file $CSUM_FILE"
	    return 2
	fi
	cp -p "$CSUM_FILE" "$ARTIFACTDIR"
	return 0
    fi
    echo "make checksums failed"
    return 2
}

# Prep the release

function git_prep_release() {
    # .po files might have changed; nothing else should have!
    if [[ -n $("${GIT}" status -uno --porcelain |egrep -v 'po/.*\.po') ]] ; then
	console_log "ERROR: Unexpected untracked files:"
	"${GIT}" status -uno --porcelain |egrep -v 'po/.*\.po' | console_log
	return 1
    fi
    # Add any of those changed files.
    ${GIT} add -u || return 1
    # Add the checksums file.
    ${GIT} add src/testpattern/Checksums/sums.$(pkg_version).zpaq || return 1
    # Commit this change
    ${GIT} commit -m"Gutenprint $(pkg_version) release" || return 1
    # Apply the tag.  Ideally we should sign the tag too.
    ${GIT} tag -a "$(pkg_tag)" -m "Gutenprint $(pkg_version) release" || return 1
}

# make distcheck-minimal
#
# We have to rebuild the tarball in any event here, so that we pick up
# the tag (to get a correct change log) and updated .po files.
# A minimal distcheck only takes about a minute; we might as well
# do a final sanity check.

function run_distcheck_minimal {
    make distcheck-minimal && return 0
    echo "Final make distcheck-minimal failed"
    return 1
}

function run_check_minimal {
    make check-minimal && return 0
    echo "Final make distcheck-minimal failed"
    return 2
}

#  Save away build
function save_build_artifacts {
    tarball=gutenprint-$(pkg_version).tar.xz
    [[ -s $tarball ]] && cp -p $tarball $ARTIFACTDIR
}

# Final release prep

function finis {
    STP_DATA_PATH=src/xml test/gen-printer-list > printer-list.$(pkg_version) || return 1
    console_log "Remainder to be done manually:"
    console_log
    console_log "  * git push"
    console_log
    console_log "  * Upload the tarball (.xz)"
    console_log
    console_log "  * Update the web site"
    console_log
    console_log "  * Merge the updated printer list into p_Supported_Printers.php"
    console_log "    and upload that"
    return 0
}

################################################################
#
# Runtime
#
################################################################

# Note that if we change the format of this timestamp we have to
# change console_log if the width changes.
#
# Unfortunately the shell built-in printf can't specify UTC.
function stamp {
    printf '%(%Y-%m-%d.%H:%M:%S%z)T'
}

function date_sec {
    printf '%(%s)T'
}

function report_progress {
    idx=$1
    shift
    case "$quiet" in
	1)
	    declare outst=.
	    [[ -n "$DONTRUN_OP" ]] && outst=-
	    echo -n $outst
	    ;;
	2)
	    ;;
	*)
	    declare outst="RUNNING[$idx]: "
	    [[ -n "$DONTRUN_OP" ]] && outst='Skipping:'
	    echo " >>> $(stamp) $outst $@"
	    ;;
    esac
}

# This allows us to log to multiple outputs, including stdout and
# (where available) file descriptors.  Ideally we'd be able to build a
# pipeline and eval it, but it's not clear that that's possible.
function log1 {
    if [[ $# == 0 || ($# == 1 && $1 == -) ]] ; then
	cat
    else
	dest="$1"
	shift
	# stdout needs to come last, because we just want to send data
	# to stdout rather than teeing off or explicitly going to a file.
	if [[ $dest == - && $# > 0 ]] ; then
	    # Protect against someone inadvertently specifying - twice!
	    if [[ $1 == - ]] ; then
		log1 "$@"
	    else
		log1 "$@" -
	    fi
	elif [[ $dest == -* ]] ; then
	    dest=${dest:1}
	    if [[ $# == 0 ]] ; then
		cat > "$dest"
	    else
		tee "$dest" | log1 "$@"
	    fi
	else
	    if [[ $# == 0 ]] ; then
		cat >> "$dest"
	    else
		tee -a "$dest" | log1 "$@"
	    fi
	fi
    fi
}

function log {
    log1 "$@" ${BUILD_VERBOSE:+-}
}

function time_delta {
    start=$1
    end=$2
    interval=$((end - start))
    h=$((interval / 3600))
    m=$(((interval % 3600) / 60))
    s=$((interval % 60))
    printf "%d:%02d:%02d" $h $m $s
}

function finish {
    status=$1
    etime=$(date_sec)
    msg=completed
    [[ $status != 0 || -n ${failedmodules[*]} ]] && msg=FAILED
    estamp=$(stamp)
    [[ $quiet = 1 ]] && echo
    if [[ -n ${failedmodules[*]} ]] ; then
	echo "The following modules failed:" | log "$top_log" -
	for f in ${failedmodules[@]} ; do
	    echo "    $f" | log "$top_log" -
	done
    fi
    echo "*** Gutenprint release build $msg at $estamp ($(time_delta $STIME $etime))" | log "$top_log" -
    echo "================================================================" | log "$top_log"
    if [[ -n $TRAVIS_MODE ]] ; then
	# We really don't want the termination message
	exec 3>&2
	exec 2>/dev/null
	kill %1
	wait %1
	exec 2>&3
	exec 3>&-
    fi
    exit $status
}

# Log the output to the console as well as the master log file and the
# per-operation log file.
#
# fd#3 (/dev/fd/3 -- let's hope we're building the package on
#      a system that supports /dev/fd, but linux does)
#      in the operation gets tied to stderr
#
# Then we timestamp the data and send it to the top-level log (which
# is not normally timestamped).
#
# Finally, we remove the existing timestamp (which relies upon the timestamp
# format, ugh) and send it to stdout where it gets picked up and timestamped
# again.
function console_log {
    if [[ -n "$@" ]] ; then
	echo "$@" | log /dev/fd/3 - | timestamp | log - "$top_log" - | cut -c26-
    else
	log /dev/fd/3 - | timestamp | log - "$top_log" - | cut -c26-
    fi
}

# Travis times out if there's no output for 10 minutes, but some things
# go silent for quite a while
function travis_deadman {
    while : ; do sleep 60; echo Mark | timestamp | log -; done
}

function timestamp {
    while read -r ; do
	echo "$(stamp) $REPLY"
    done
}

# Run one operation.
function runit {
    cmdname=$1
    cmd="$@"
    fcounter=$(printf "%02d" $counter)
    local_logdir="$LOGDIR/$fcounter.${cmd// /_/}"
    mkdir -p $local_logdir
    logfile="$local_logdir/Master"
    [[ -n $DONTRUN_OP ]] && logfile=/dev/null
    sstime=$(date_sec)
    ssstamp=$(stamp)
    status=0
    msg=completed
    echo "----------------------------------------------------------------" | log "-$logfile" "$top_log"
    if [[ -z $DONTRUN_OP ]] ; then
	echo "$cmdname started at $ssstamp" | log "$logfile" "$top_log"
	echo "Command: $cmd" | log "$logfile" "$top_log"
	echo "Log file: ${logfile#${LOGDIR}/}" | log "$top_log"
    else
	echo "$cmdname SKIPPED" | log "$top_log"
    fi
    report_progress $fcounter $cmdname

    if [[ -z $DONTRUN_OP ]] ; then
	STP_TEST_LOG_PREFIX="$local_logdir/" $cmd </dev/null 3>&2 2>&1 | timestamp | log "$logfile"
	status=${PIPESTATUS[0]}
	(( $status > 0 )) && msg=FAILED
	for f in $local_logdir/* ; do
	    [[ -f $f && ! -s $f ]] && rm -f $f
	done
    else
	msg='(SKIPPED)'
    fi
    setime=$(date_sec)
    sestamp=$(stamp)
    if [[ -z $DONTRUN_OP ]] ; then
	echo "$cmd $msg at $sestamp ($(time_delta $sstime $setime))" | log "$logfile" "$top_log"
	echo "----------------------------------------------------------------" | log "$logfile" "$top_log"
    fi
    counter=$((counter+1))

    return $status
}

declare -a OPERATIONS=(preflight
		       check_git
		       run_build
		       check_git_tag
		       check_git_builds
		       run_valgrind_minimal
		       run_distcheck_fast
		       run_valgrind
		       run_full
		       run_checksums
		       git_prep_release
		       run_distcheck_minimal
		       save_build_artifacts
		       finis)

[[ -n "$@" ]] && OPERATIONS=("$@")

declare HOST=$(uname -n)
declare SSTAMP=$(stamp)
declare STIME=$(date_sec)
declare TOPLOGDIR=${STP_LOG_DIR:-"$ROOT/BuildLogs"}
declare LOGDIR="$TOPLOGDIR/Log.${SSTAMP// /_}"
[[ -n $STP_LOG_NO_SUBDIR ]] && LOGDIR=$TOPLOGDIR
mkdir -p $LOGDIR
if [[ -z $STP_LOG_NO_SUBDIR ]] ; then
     if [[ -L $TOPLOGDIR/Current ]] ; then
	 rm -f $TOPLOGDIR/Previous
	 mv $TOPLOGDIR/Current $TOPLOGDIR/Previous
	 rm -f "$TOPLOGDIR/Current"
     fi
    ln -s $(basename "$LOGDIR") "$TOPLOGDIR/Current"
fi
declare ARTIFACTDIR="$LOGDIR/Artifacts"
mkdir -p $ARTIFACTDIR
export ARTIFACTDIR

skip_ops=${STP_BUILD_SKIP//,/ }

declare -A SKIP_OPS

for o in $skip_ops ; do
    SKIP_OPS[$o]=1
done

declare counter=1
declare top_log="$LOGDIR/00.Master"
declare git_dirty=

if [[ -n $TRAVIS_MODE ]] ; then
    export BUILD_VERBOSE=1
    travis_deadman&
fi

echo "================================================================" | log "-$top_log"
echo "*** Gutenprint release build started at $SSTAMP on $HOST" | log "$top_log" -
echo "Directory: $ROOT" | log "$top_log" -
echo "Log Directory: ${LOGDIR#${ROOT}/}" | log "$top_log" -
echo "Parallelism: $STP_PARALLEL" | log "$top_log" -
[[ -n $("$GIT" status --porcelain -uno) ]] && git_dirty=' (dirty)'
echo "Git revision: $("$GIT" rev-parse @)$git_dirty" | log "$top_log" -

declare -i runstatus=0
for op in ${OPERATIONS[@]} ; do
    DONTRUN_OP=${DONTRUN}${SKIP_OPS[$op]} runit $op
    case "$?" in
	0) true                                            ;;
	2) failedmodules=($failedmodules $op); runstatus=1 ;;
	*) failedmodules=($failedmodules $op); finish 1    ;;
    esac
done

finish $runstatus