aboutsummaryrefslogtreecommitdiff
path: root/sbrun
blob: 7ec3519105bbba926bae42a4ee98c77eb6450acc (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
#!/bin/bash

# Configurables:

TMP=${TMP:-/tmp/SBo}
OUTPUT=${OUTPUT:-/tmp}
BUILDLOG=${BUILDLOG:-build.log}
DEFAULT_MAKEFLAGS="-j8"

# End of configurables. It's probably best not to configure TMP or
# OUTPUT here (use the environment instead) anyway. Also it's probably
# convenient to add build.log to .git/info/exclude.

# If we're not running as root, re-exec as root, with args.
# Anything sbrun expects to possibly inherit from the caller's environment
# must be explicity set here as sudo will strip them from the environment
# before executing anything.
if [ "$(id -u)" != "0" ]; then
	exec sudo \
		TMP="$TMP" \
		OUTPUT="$OUTPUT" \
		MAKEFLAGS="$MAKEFLAGS" \
		BUILDLOG="$BUILDLOG" \
		"$0" "$@"
fi

# Inherit MAKEFLAGS from env, if present.
MAKEFLAGS="${MAKEFLAGS:-$DEFAULT_MAKEFLAGS}"

# Defaults, changed by -options.
NETWORK="no"
TRACK="yes"
STRACE=""
CLEANUP="no"
SRCSH="no"
PKGSH="no"
LOGDIR=""
NSENTER=""
TRACKFS=""

SELF=$(basename $0)

# unshare and nsenter use this. It's theoretically better to use
# an unpredictable filename (not one based on the PID), but anyone
# able to mess with /mnt already has root access.
NONET_PATH=/mnt/nonet.$SELF.$$

# Possible future options:
# -f  Run script with fakeroot. (still need root/sudo for unshare/nsenter,
#     and trackfs won't track failed writes, maybe this isn't that useful?)
# -I  Install package after it's built. Could count as scope creep.

long_help() {
   # note: root's pager is used, not the user's, since we use sudo.
	# not going to care about this one.
	cat <<EOF | ${PAGER:-less}
$SELF: paranoid SlackBuild wrapper

$SELF runs the SlackBuild script in the current directory,
with an optional custom environment.

By default, the SlackBuild can't access the network, and filesystem
activity is tracked: writes to system directories are flagged and
reported. Also, a complete log of the build's standard output
and standard error is written to "$BUILDLOG".

If $SELF is called as a non-root user, it re-executes itself via
sudo. If you hate sudo, just run $SELF as root.

$SELF is designed for use with SBo scripts, but will work for any
SlackBuild (it doesn't refer to the SBo .info file).

You'll want to install system/trackfs from SBo for filesystem tracking
to work.

$SELF written by B. Watson (yalhcru@gmail.com) and released
under the WTFPL. See http://www.wtfpl.net/txt/copying/ for details.

Usage: $SELF [-jN] [-n] [script] [variable=value ...]

Options may not be bundled (use -t -n, not -tn or -nt). All options
beginning with - must occur before [script] or [variable=value].

-jN  Run N make jobs in parallel. Default is to use MAKEFLAGS from
     the environment if set, otherwise "$DEFAULT_MAKEFLAGS". If a SlackBuild fails
     without -j1, this is a bug in the SlackBuild and you should ask
     its maintainer to add -j1 to the make command in the script.

-n   Allow the SlackBuild to access the network. If a SlackBuild
     fails without this flag, that's a bug in the SlackBuild and
     should be reported to its maintainer (EMAIL in the .info file).

-t   Don't use trackfs to watch for writes to system files. If a
     SlackBuild fails without this flag, it's a bug in either
     $SELF or trackfs, and should be reported to the maintainer
     at yalhcru@gmail.com.

-s   Run the script with strace. This option implies -t (trackfs
     will be disabled). The strace log will be written to the
     current directory as "strace.out".

-S   Like -s, but using strace's -ff option. Each child process executed
     gets its own strace log file. They will be named "strace.out.<pid>".

-x   Run the script with "sh -x", enables shell command tracing.

-i   Run an interactive shell in the source directory, after the script
     completes *successfully* (nothing happens if it fails). Useful for
     development, e.g. place 'exit 0' in the script wherever you need to
     examine the state of the source directory. May not work as expected
     if the SlackBuild creates multiple directories under \$TMP, or if
     multiple SlackBuilds are being run simultaneously.

-p   Run an interactive shell in the \$PKG directory, after the script
     completes (successfully or otherwise, provided the directory
     exists). Useful for development.

-c   Clean up (remove) source and package directories after the
     build completes. This option overrides \$TMP from the environment.

-h, --help
     Show short usage message and exit.

-H   Show long help message (you're reading it now) and exit.

[script]
     Run this script instead of the .SlackBuild script. Useful for
     development, I hope. Useful also for Slack-derived distros that
     use something other than .SlackBuild for their script names.

[variable=value ...]
   All arguments containing an = (and not beginning with -) are passed as
   part of the SlackBuild script's environment. Options beginning with -
   must occur before environment variables. Example:

     $SELF -j1 SDL2=no DOCS=yes

   Leave off the -j1 to use the default number of jobs.

After the SlackBuild exits, any files written to outside of \$TMP,
\$OUTPUT, /tmp, /var/tmp, or /root/.ccache (collectively referred to
as "the sandbox") are logged to stdout. See trackfs(1) for details of
the log format, but any write outside the sandbox means a bug in the
SlackBuild and should be reported to its maintainer.

The exit status of $SELF is the exit status of the SlackBuild.

Note: the current directory needs to be writable, since the log and
(with -s/-S) strace output are written there.

======================================================================
The rest of this help message is a long-winded discussion that doesn't
include any more usage information. Feel free to stop reading at any
time :)
======================================================================

Why does sbrun exist? Why not use sbopkg or sbotools?  sbrun is targeted
more towards a SlackBuild developer/maintainer than an end user. My
workflow is to edit the script in one terminal and repeatedly execute it
in another. If you're editing a SlackBuild, you keep running it over &
over again. So I wrote a 1-liner shell script that did this:

sudo sh ./\$( pwd | sed 's,.*/,,' ).SlackBuild

I have /sbin:/usr/sbin in my user's PATH so sudo works fine, but it's a
PITA to pass environment variables using the above script (sudo strips
them out of the env). So I made the script take arguments, and treat
those as env vars (place them between 'sudo' and 'sh').

Then I added MAKEFLAGS support to it (-jN option). Then I found out some
of my builds were writing outside of $TMP (due to either my own mistakes,
or upstream bugs) and decided I needed a way to reliably detect that,
hence the trackfs stuff. Also someone on the mailing list posted output
from running a SlackBuild under bubblewrap, which prevented network access
the script was trying to do. Which seems like a nifty feature to have,
but bubblewrap is overkill for just running a shell script.

The strace and sh -x options were added because those are things I do
fairly often with buggy SlackBuilds. The elapsed time display is a nice
convenience (I complain that "This thing takes 2 hours to build!" when
really it's closer to 1 hour).

The -i option exists because I used to be in the habit of placing 'exec
bash -login' part of the way through a script as a way to get a shell in
the source dir... but this stopped being possible once I added build.log
to sbrun. So now I can stick 'exit 0' part way through the script and use
'sbrun -i' to get the same thing. -p was added for similar reasons.

The -c option is the only "end user" option, really. I mostly use it
for building dependencies maintained by other people, not my own stuff.

Basically, sbrun started out as a lazy typist's tool, and grew into
something more generally useful. It's lower-level than sbotools or sbopkg,
but more convenient than just executing ./*.SlackBuild. It requires no
initial setup or configuration (it's a self-contained shell script),
except you have to install trackfs.

Since it's *not* intended to replace sbotools or sbopkg, sbrun doesn't
do any of these things:

- download source files.
- check source file md5sums.
- allow building multiple packages at once (queue files).
- install/upgrade/remove packages (it *just* builds them).
- dependency resolution.
- sync the repo, or even have any concept of a repo (it only deals
  with a single SlackBuild script, in the current directory).
- anything to do with .info files. Nothing about sbrun is SBo-specific,
  it'll work with Pat's or AlienBOB's or anyone else's scripts (which
  is why it's called sbrun and not sborun).

Finally, a helpful hint: If you use git to push to SBo, you can't
add anything to .gitignore since it's tracked by git. But you can
use .git/info/exclude for the same purpose. Add build.log and maybe
strace.out there.
EOF
}

show_help() {
	# don't use warn here, log isn't open yet.
	if [ -n "$1" ]; then
		echo "$SELF: unknown option '$1'" 2>&1
	fi
	cat <<EOF
$SELF: paranoid SlackBuild wrapper.

$SELF written by B. Watson (yalhcru@gmail.com) and released
under the WTFPL. See http://www.wtfpl.net/txt/copying/ for details.

Usage: $SELF [-option [-option ...]] [script] [variable=value ...]

-jN  Run N make jobs in parallel.
-n   Allow the SlackBuild to access the network.
-t   Don't use trackfs to watch for writes to system files.
-s   Run the script with strace, output in "strace.out".
-S   Run the script with strace -ff, outputs in "strace.out.<pid>".
-x   Run the script with "sh -x", enables shell command tracing.
-c   Clean up (remove) source and package dirs after build completes.
-i   Run an interactive shell in the source directory.
-p   Run an interactive shell in the \$PKG directory.
-h, --help
     Show short usage message (you're reading it now) and exit.
-H   Show long help message and exit.
script
     Run this script instead of the default .SlackBuild script.
variable=value ...
     Passed to script as environment variables.
EOF
}

# maybe add 2>/dev/null to these, but for now I wanna know if they fail.
cleanup_nonet() {
	if [ "$NETWORK" = "no" ]; then
		umount $NONET_PATH
		rm -f $NONET_PATH
	fi
}

cleanup_log() {
	[ -n "$LOGDIR" ] && rm -rf "$LOGDIR"
}

cleanup_build() {
	[ "$CLEANUP" = "yes" ] && ( rm -rf "$TMP" ; rm -f $BUILDLOG )
}

# Add a dir to $PATH, if not already present. This is actually kinda
# pointless, it would work just as well to always add dirs to PATH
# even if they're redundant.
ensure_path() {
	if ! echo "$PATH" | sed 's,:,\n,g' | grep -q "^$1\$"; then
		#echo "$1 not in PATH, adding"
		export PATH="$1:$PATH"
	fi
}

# Handle ^C gracefully. TODO: the exit status should be 128 plus
# the number of the signal received. The hard-coded 130 means SIGINT,
# the ^C signal, but we trap other signals too. Maybe use:
# http://stackoverflow.com/questions/2175647/is-it-possible-to-detect-which-trap-signal-in-bash
signal_handler() {
	cleanup_log
	cleanup_nonet
	cleanup_build
	exit 130
}

# Print a number of seconds as either MM:SS (if less than 1 hour)
# or HH:MM:SS (if >= 1 hour). This function could almost be replaced
# by:  TZ=GMT printf '%(%H:%M:%S)T\n' "$1"
# ...except print_hms doesn't display the hours if they're 00, and using
# printf that way won't handle durations longer than 23:59:59 because
# it's trying to print a time of day (24:00:00 would be 00:00:00 of the
# next day). Hopefully no SlackBuild takes over a day to run, but you
# never know...
print_hms() {
	local sec="$1" hrs min

	hrs=$(( $sec / 3600 ))
	sec=$(( $sec % 3600 ))

	min=$(( $sec / 60 ))
	sec=$(( $sec % 60 ))

	if [ "$hrs" -gt "0" ]; then
		printf '%02d:%02d:%02d\n' $hrs $min $sec
	else
		printf '%02d:%02d\n' $min $sec
	fi
}

# perl-flavoured error messenger
warn() {
	echo "$SELF:" "$@" 1>&2
	echo "$SELF:" "$@" >> $BUILDLOG
}

# Suicide squad, attack!
die() {
	warn "$@"
	exit 1
}

### main()

# if these are in $PATH, 99.99% of all SBo builds will run
# correctly under sudo. Or maybe even 100%. At least, I can't
# remember running into problems, for quite a few years now.
ensure_path /sbin
ensure_path /usr/sbin
ensure_path /usr/share/texmf/bin

# parse -options
while printf -- "$1" | grep -q ^-; do
	case "$1" in
		-j*)              MAKEFLAGS="$1"          ;;
		-n)               NETWORK=yes             ;;
		-t)               TRACK=no                ;;
		-s)               TRACK=no; STRACE=-f     ;;
		-S)               TRACK=no; STRACE=-ff    ;;
		-x)               X="-x"                  ;;
		-c)               CLEANUP="yes"           ;;
		-i)               SRCSH="yes"             ;;
		-p)               PKGSH="yes"             ;;
		-h|-help|--help)  show_help ; exit 0      ;;
		-H)               long_help ; exit 0      ;;
		-*)               show_help "$1"; exit 1  ;;
	esac
	shift
done

# warn and die append to the log, make sure it starts out empty.
# This is the only place we use tee $BUILDLOG (everything else appends).
{
echo -n "== $SELF starting up at "
date
echo -n "== directory: "
pwd
echo "== command: $0" "$@"
echo
} | tee $BUILDLOG

# rest of arg parsing can use warn or die.
if echo "$1" | grep -qv '='; then
	SCRIPT="$1"
	shift
	#[ ! -e "$SCRIPT" ] && warn "$SCRIPT not found, I hope you know what you're doing!"
fi

# $ENV is only for showing to the user
ENV="MAKEFLAGS=$MAKEFLAGS"
export MAKEFLAGS TMP OUTPUT

# Add rest of args to environment. The echo|cut and eval stuff allows
# spaces to occur in the values. There is probably a better modern-bash
# way to do this, but (to me anyway) it'll be less readable.
for arg; do
	if echo "$arg" | grep -qv '='; then
		die "invalid/unknown argument '$1', try -h for help or -H for long help."
	else
		ENV="$ENV $arg"
		#eval export "$arg" # works but doesn't allow spaces
		var="$( echo "$arg" | cut -d= -f1 )"
		val="$( echo "$arg" | cut -d= -f2 )"
		eval "export $var='$val'"
	fi
done

# The easy way to remove the source and PKG dirs after the
# script runs is to guarantee they'll be the only things in
# $TMP. Normally, we don't create the $TMP dir, so we can
# catch 'script fails to create $TMP dir' errors. But with -c,
# we don't care about troubleshooting so much, and mktemp is
# the way to go.
if [ "$CLEANUP" = "yes" ]; then
	TMP="$( mktemp -d /tmp/sbrun.build.XXXXXX )"
	if [ -z "$TMP" ] || [ ! -d "$TMP" ]; then
		die "Can't create temp build dir in /tmp, bailing"
	fi
fi

# I wasn't gonna trap signals, but I can't break myself of the habit
# of hitting ^C.
# TODO: we should be trapping more signals here...
# TODO: find out why trackfs sometimes segfaults when I hit ^C. No
#       harm done (it was already killed by SIGINT), just irritating.
trap signal_handler INT TERM

if [ "$TRACK" = "yes" ]; then
	if ! /bin/which trackfs &>/dev/null; then
		warn "File tracking enabled, but trackfs not installed!"
		die "Install system/trackfs or re-run $SELF with -t."
	fi

	LOGDIR="$( mktemp -d /tmp/sbrun.XXXXXX )"
	if [ -z "$LOGDIR" ] || [ ! -d "$LOGDIR" ]; then
		die "Can't create temp log dir in /tmp, bailing"
	fi

	LOG=$LOGDIR/log

	# Fun fact: trackfs uses GNU-style -- to mean "no more options",
	# but it's undocumented in the man page and --help output.
	# The readlink stuff is here in case $TMP or $OUTPUT has a symlink
	# in its path: trackfs will log the real path, with the links resolved.
	TRACKFS=\
"trackfs -l $LOG \
	-I$( readlink -f "$TMP" )\\* \
	-I$( readlink -f "$OUTPUT")\\* \
	-I/tmp\\* \
	-I/var/tmp\* \
	-I/root/.ccache\\* \
	--"

fi

if [ "$STRACE" != "" ]; then
	TRACKFS="strace $STRACE -ostrace.out"
fi

# Used to do this, but it doesn't allow for cases where the directory
# has been renamed (foo.SlackBuild in a dir called foo.testing or foo.old).
#SCRIPT="./$( pwd | sed 's,.*/,,' ).SlackBuild"

# This is better, but during development, a user might have copies of the
# script named foo.old.SlackBuild and foo.new.SlackBuild, so not perfect.
# To allow for this, we now take an optional script name on the command line.
SCRIPT="${SCRIPT:-$( /bin/ls ./*.SlackBuild | head -1 )}"

if [ ! -e "$SCRIPT" ]; then
	die "$SCRIPT not found, bailing"
fi

{
	echo "Running $SCRIPT, logging to $BUILDLOG"
	echo "Environment:    $ENV"
	echo "File tracking:  $TRACK"
	echo "Network access: $NETWORK"
	if [ "$STRACE" != "" ]; then
		echo "strace log:     strace.out"
	fi
	echo
} | tee -a $BUILDLOG

# Set up no-network namespace. This isn't foolproof, there are probably
# ways for a script being run by root to escape the namespace, but
# a script that did that would hopefully never get approved by our
# beloved moderators.
if [ "$NETWORK" = "no" ]; then
	touch $NONET_PATH
	unshare --net=$NONET_PATH ifconfig lo 127.0.0.1 up
	NSENTER="nsenter --net=$NONET_PATH"
fi

START_TIME="$( date +%s )"

# Actually run the script. Note that the 'tee' command isn't being
# tracked by trackfs. The rigmarole with $? and RET might not be the
# best way to get the exit status, TODO: see if I can do this cleaner.
# Also, using { } instead of ( ) utterly fails.
( eval $NSENTER $TRACKFS sh $X $SCRIPT 2>&1; echo "$?" > $LOGDIR/ret ) | tee -a $BUILDLOG
RET="$( cat $LOGDIR/ret )"

END_TIME="$( date +%s )"

echo "$SCRIPT exit status: $RET" | tee -a $BUILDLOG

{
	echo -n "Elapsed time: "
	print_hms $(( $END_TIME - $START_TIME ))
} | tee -a $BUILDLOG

cleanup_nonet

if [ "$TRACK" = "yes" ]; then
	if [ -s $LOG ]; then
		warn "WARNING: files altered outside the sandbox:"
		cat $LOG 1>&2
		cat $LOG >> $BUILDLOG
	fi

	cleanup_log
fi

# spawn shell(s) if requested. -i and -p are not mutually exclusive.

# TODO: maybe do this cleaner, using trackfs?
if [ "$SRCSH" = "yes" ]; then
	if [ "$RET" != "0" ]; then
		warn "Script failed (status $RET), ignoring -i option"
	else
		SRCDIR="$( /bin/ls -td $TMP/*/ | grep -v /package- | head -1 )"
		( cd $SRCDIR && bash -login )
	fi
fi

# PRGNAM is problematic. We don't want to use $( basename $( pwd ) )
# because the directory might have been renamed (foo => foo.testing or
# foo.old). Reading the .info file is no good (this might not be an SBo
# build). For now, extract it from the script name, but eventually this
# won't work because I want to support passing a script name someday
# instead of hard-coding .SlackBuild.
if [ "$PKGSH" = "yes" ]; then
	PRGNAM="$( echo $SCRIPT | sed 's,^\./\(.*\)\.SlackBuild$,\1,' )"
	PKG=$TMP/package-$PRGNAM
	if [ -d "$PKG" ]; then
		( cd $PKG && bash -login )
	else
		warn "$PKG not found, ignoring -p option"
	fi
fi

cleanup_build

# Our return status is that of the SlackBuild.
exit $RET