aboutsummaryrefslogtreecommitdiff
path: root/sbrun
blob: 407bd64bfc84d79f0996704f3a173da0992d3886 (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
#!/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 .gitignore.

# 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="no"
CLEANUP="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.$$

# 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 -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). 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).

# 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.
# -H  long help (the existing help is long already, -h would be an extract)

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] [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".

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

-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.

All arguments 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.
EOF
}

show_help() {
	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 [-jN] [-n] [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".
-x   Run the script with "sh -x", enables shell command tracing.
-c   Clean up (remove) source and package dirs after build completes.
-h, --help
     Show short usage message (you're reading it now) and exit.
-H   Show long help message and exit.
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=yes ;;
		-x)               X="-x"               ;;
		-c)               CLEANUP="yes"        ;;
		-h|-help|--help)  show_help ; exit 0   ;;
		-H)               long_help ; exit 0   ;;
		-*)               show_help ; exit 1   ;;
	esac
	shift
done

# warn and die append to the log, make sure it starts out empty.
> $BUILDLOG

# 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

# $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
	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'"
done

# 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" = "yes" ]; then
	TRACKFS="strace -f -ostrace.out"
fi

SCRIPT="./$( pwd | sed 's,.*/,,' ).SlackBuild"
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" = "yes" ]; 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.
eval $NSENTER $TRACKFS sh $X $SCRIPT 2>&1 | tee -a $BUILDLOG
RET=$?
echo "$SCRIPT exit status: $RET" | tee -a $BUILDLOG

END_TIME="$( date +%s )"

{
	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

cleanup_build

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