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
|
#!/bin/sh
#
# Create a Docker container from an existing image of the built software
# environment, but with the source, data and build (analysis) directories
# directly within the host file system. This script is assumed to be run in
# the top project source directory (that has 'README.md' and
# 'paper.tex'). If not, use the '--source-dir' option to specify where the
# Maneage'd project source is located.
#
# Usage:
#
# - When you are at the top Maneage'd project directory, you can run this
# script like the example below. Just set all the '/PATH/TO/...'
# directories (see below for '--tmp-dir'). See the items below for
# optional values.
#
# ./reproduce/software/containers/docker.sh --shm-size=15gb \
# --software-dir=/PATH/TO/SOFTWARE/TARBALLS \
# --build-dir=/PATH/TO/BUILD/DIRECTORY
#
# - Non-mandatory options:
#
# - If you already have the input data that is necessary for your
# project's, use the '--input-dir' option to specify its location
# on your host file system. Otherwise the necessary analysis
# files will be downloaded directly into the build
# directory. Note that this is only necessary when '--build-only'
# is not given.
#
# - The '--software-dir' is only useful if you want to build a
# container. Even in that case, it is not mandatory: if not
# given, the software tarballs will be downloaded (thus requiring
# internet).
#
# - To avoid having to set the directory(s) every time you want to
# start the docker environment, you can put this command (with the
# proper directories) into a 'run.sh' script in the top Maneage'd
# project source directory and simply execute that. The special name
# 'run.sh' is in Maneage's '.gitignore', so it will not be included
# in your git history by mistake.
#
# Known problems:
#
# - As of 2025-04-06 the log file containing the output of the 'docker
# build' command that configures the Maneage'd project does not keep
# all the output (which gets clipped by Docker). with a "[output
# clipped, log limit 2MiB reached]" message. We need to find a way to
# fix this (so nothing gets clipped: useful for debugging).
#
# Copyright (C) 2021-2025 Mohammad Akhlaghi <mohammad@akhlaghi.org>
#
# This script 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 3 of the License, or (at your
# option) any later version.
#
# This script 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 script. If not, see <http://www.gnu.org/licenses/>.
# Script settings
# ---------------
# Stop the script if there are any errors.
set -e
# Default option values
jobs=
quiet=0
source_dir=
build_only=
image_file=""
shm_size=20gb
scriptname="$0"
project_shell=0
container_shell=0
project_name=maneaged
base_name=maneage-base
base_os=debian:stable-slim
print_help() {
# Print the output.
cat <<EOF
Usage: $scriptname [OPTIONS]
Top-level script to build and run a Maneage'd project within Docker.
Host OS directories (to be mounted in the container):
-b, --build-dir=STR Dir. to build in (only analysis in host).
-i, --input-dir=STR Dir. of input datasets (optional).
-s, --software-dir=STR Directory of necessary software tarballs.
--source-dir=STR Directory of source code (default: 'pwd -P').
Docker images
--base-os=STR Base OS name (default: '$base_os').
--base-name=STR Base OS docker image (default: $base_name).
--project-name=STR Project's docker image (default: $project_name).
--image-file=STR [Docker only] Load (if given file exists), or
save (if given file does not exist), the image.
For saving, the given name has to have an
'.tar.gz' suffix.
Interactive shell
--project-shell Open the project's shell within the container.
--container-shell Open the container shell.
Operating mode:
--quiet Do not print informative statements.
-?, --help Give this help list.
--shm-size=STR Passed to 'docker build' (default: $shm_size).
-j, --jobs=INT Number of threads to use in each phase.
--build-only Just build the container, don't run it.
Mandatory or optional arguments to long options are also mandatory or
optional for any corresponding short options.
Maneage URL: https://maneage.org
Report bugs to mohammad@akhlaghi.org
EOF
}
on_off_option_error() {
if [ "x$2" = x ]; then
echo "$scriptname: '$1' doesn't take any values"
else
echo "$scriptname: '$1' (or '$2') doesn't take any values"
fi
exit 1
}
check_v() {
if [ x"$2" = x ]; then
printf "$scriptname: option '$1' requires an argument. "
printf "Try '$scriptname --help' for more information\n"
exit 1;
fi
}
while [ $# -gt 0 ]
do
case $1 in
# OS directories
-b|--build-dir) build_dir="$2"; check_v "$1" "$build_dir"; shift;shift;;
-b=*|--build-dir=*) build_dir="${1#*=}"; check_v "$1" "$build_dir"; shift;;
-b*) build_dir=$(echo "$1" | sed -e's/-b//'); check_v "$1" "$build_dir"; shift;;
-i|--input-dir) input_dir="$2"; check_v "$1" "$input_dir"; shift;shift;;
-i=*|--input-dir=*) input_dir="${1#*=}"; check_v "$1" "$input_dir"; shift;;
-i*) input_dir=$(echo "$1" | sed -e's/-i//'); check_v "$1" "$input_dir"; shift;;
-s|--software-dir) software_dir="$2"; check_v "$1" "$software_dir"; shift;shift;;
-s=*|--software-dir=*) software_dir="${1#*=}"; check_v "$1" "$software_dir"; shift;;
-s*) software_dir=$(echo "$1" | sed -e's/-s//'); check_v "$1" "$software_dir"; shift;;
--source-dir) source_dir="$2"; check_v "$1" "$source_dir"; shift;shift;;
--source-dir=*) source_dir="${1#*=}"; check_v "$1" "$source_dir"; shift;;
# Container options.
--base-name) base_name="$2"; check_v "$1" "$base_name"; shift;shift;;
--base-name=*) base_name="${1#*=}"; check_v "$1" "$base_name"; shift;;
# Interactive shell.
--project-shell) project_shell=1; shift;;
--project_shell=*) on_off_option_error --project-shell;;
--container-shell) container_shell=1; shift;;
--container_shell=*) on_off_option_error --container-shell;;
# Operating mode
--quiet) quiet=1; shift;;
--quiet=*) on_off_option_error --quiet;;
-j|--jobs) jobs="$2"; check_v "$1" "$jobs"; shift;shift;;
-j=*|--jobs=*) jobs="${1#*=}"; check_v "$1" "$jobs"; shift;;
-j*) jobs=$(echo "$1" | sed -e's/-j//'); check_v "$1" "$jobs"; shift;;
--build-only) build_only=1; shift;;
--build-only=*) on_off_option_error --build-only;;
--shm-size) shm_size="$2"; check_v "$1" "$shm_size"; shift;shift;;
--shm-size=*) shm_size="${1#*=}"; check_v "$1" "$shm_size"; shift;;
-'?'|--help) print_help; exit 0;;
-'?'*|--help=*) on_off_option_error --help -?;;
# Output file
--image-file) image_file="$2"; check_v "$1" "$image_file"; shift;shift;;
--image-file=*) image_file="${1#*=}"; check_v "$1" "$image_file"; shift;;
# Unrecognized option:
-*) echo "$scriptname: unknown option '$1'"; exit 1;;
esac
done
# Sanity checks
# -------------
#
# Make sure that the build directory is given and that it exists.
if [ x$build_dir = x ]; then
printf "$scriptname: '--build-dir' not provided, this is the location "
printf "that all built analysis files will be kept on the host OS\n"
exit 1;
else
if ! [ -d $build_dir ]; then
printf "$scriptname: '$build_dir' (value to '--build-dir') doesn't "
printf "exist\n"; exit 1;
fi
fi
# The temporary directory to place the Dockerfile.
tmp_dir="$build_dir"/temporary-docker-container-dir
# Directory preparations
# ----------------------
#
# If the host operating system has '/dev/shm', then give Docker access
# to it also for improved speed in some scenarios (like configuration).
if [ -d /dev/shm ]; then shm_mnt="-v /dev/shm:/dev/shm";
else shm_mnt=""; fi
# If the following directories do not exist within the build directory,
# create them to make sure the '--mount' commands always work and
# that any file. Ideally, the 'input' directory should not be under the 'build'
# directory, but if the user hasn't given it then they don't care about
# potentially deleting it later (Maneage will download the inputs), so put
# it in the build directory.
analysis_dir="$build_dir"/analysis
if ! [ -d $analysis_dir ]; then mkdir $analysis_dir; fi
# If no '--source-dir' was given, set it to the output of 'pwd -P' (to get
# the path without potential symbolic links) in the running directory.
if [ x"$source_dir" = x ]; then source_dir=$(pwd -P); fi
# Only when an an input directory is given, we need the respective 'mount'
# option for the 'docker run' command.
input_dir_mnt=""
if ! [ x"$input_dir" = x ]; then
input_dir_mnt="-v $input_dir:/home/maneager/input"
fi
# If no '--jobs' has been specified, use the maximum available jobs to the
# operating system.
if [ x$jobs = x ]; then jobs=$(nproc); fi
# [DOCKER-ONLY] Make sure the user is a member of the 'docker' group:
glist=$(groups $(whoami) | awk '/docker/')
if [ x"$glist" = x ]; then
printf "$scriptname: you are not a member of the 'docker' group "
printf "You can run the following command as root to fix this: "
printf "'usermod -aG docker $(whoami)'\n"
exit 1
fi
# [DOCKER-ONLY] Function to check the temporary directory for building the
# base operating system docker image. It is necessary that this directory
# be empty because Docker will inherit the sub-directories of the directory
# that the Dockerfile is located in.
tmp_dir_check () {
if [ -d $tmp_dir ]; then
printf "$scriptname: '$tmp_dir' already exists, please "
printf "delete it and re-run this script. This is a temporary "
printf "directory only necessary when building a Docker image "
printf "and gets deleted automatically after a successful "
printf "build. The fact that it remains hints at a problem "
printf "in a previous attempt to build a Docker image\n"
exit 1
else
mkdir $tmp_dir
fi
}
# Base operating system
# ---------------------
#
# If the base image does not exist, then create it. If it does, inform the
# user that it will be used.
if docker image list | grep $base_name &> /dev/null; then
if [ $quiet = 0 ]; then
printf "$scriptname: info: base OS docker image ('$base_name') "
printf "already exists and will be used. If you want to build a "
printf "new base OS image, give a new name to '--base-name'. "
printf "To remove this message run with '--quiet'\n"
fi
else
# In case an image file is given, load the environment from that (no
# need to build the environment from scratch).
if ! [ x"$image_file" = x ] && [ -f "$image_file" ]; then
docker load --input $image_file
else
# Build the temporary directory.
tmp_dir_check
# Build the Dockerfile.
uid=$(id -u)
cat <<EOF > $tmp_dir/Dockerfile
FROM $base_os
RUN useradd -ms /bin/sh --uid $uid maneager; \\
printf '123\n123' | passwd maneager; \\
printf '456\n456' | passwd root
RUN apt update; apt install -y gcc g++ wget; echo 'export PS1="[\[\033[01;31m\]\u@\h \W\[\033[32m\]\[\033[00m\]]# "' >> ~/.bashrc
USER maneager
WORKDIR /home/maneager
RUN mkdir build; mkdir build/analysis; echo 'export PS1="[\[\033[01;35m\]\u@\h \W\[\033[32m\]\[\033[00m\]]$ "' >> ~/.bashrc
EOF
# Build the base-OS container and delete the temporary directory.
curdir="$(pwd)"
cd $tmp_dir
docker build ./ \
-t $base_name \
--shm-size=$shm_size
cd "$curdir"
rm -rf $tmp_dir
fi
fi
# Maneage software configuration
# ------------------------------
#
# Having the base operating system in place, we can now construct the
# project's docker file.
if docker image list | grep $project_name &> /dev/null; then
if [ $quiet = 0 ]; then
printf "$scriptname: info: project's image ('$project_name') "
printf "already exists and will be used. If you want to build a "
printf "new project image, give a new name to '--project-name'. "
printf "To remove this message run with '--quiet'\n"
fi
else
# Build the temporary directory.
tmp_dir_check
df=$tmp_dir/Dockerfile
# The only way to mount a directory inside the Docker build environment
# is the 'RUN --mount' command. But Docker doesn't recognize things
# like symbolic links. So we need to copy the project's source under
# this temporary directory.
sdir=source
mkdir $tmp_dir/$sdir
dsr=/home/maneager/source-raw
cp -r $source_dir/* $source_dir/.git $tmp_dir/$sdir
# Start constructing the Dockerfile.
#
# Note on the printf's '\x5C\n' part: this will print out as a
# backslash at the end of the line to allow easy human readability of
# the Dockerfile (necessary for debugging!).
echo "FROM $base_name" > $df
printf "RUN --mount=type=bind,source=$sdir,target=$dsr \x5C\n" >> $df
# If a software directory was given, copy it and add its line.
tsdir=tarballs-software
dts=/home/maneager/tarballs-software
if ! [ x"$software_dir" = x ]; then
# Make the directory to host the software and copy the contents
# that the user gave there.
mkdir $tmp_dir/$tsdir
cp -r "$software_dir"/* $tmp_dir/$tsdir/
printf " --mount=type=bind,source=$tsdir,target=$dts \x5C\n" >> $df
fi
# Construct the rest of the 'RUN' command.
printf " cp -r $dsr /home/maneager/source; \x5C\n" >> $df
printf " cd /home/maneager/source; \x5C\n" >> $df
printf " ./project configure --jobs=$jobs \x5C\n" >> $df
printf " --build-dir=/home/maneager/build \x5C\n" >> $df
printf " --input-dir=/home/maneager/input \x5C\n" >> $df
printf " --software-dir=$dts; \x5C\n" >> $df
# We are deleting the '.build/software/tarballs' directory because this
# directory is not relevant for the analysis of the project. But in
# case any tarball was downloaded, it will consume space within the
# container.
printf " rm -rf .build/software/tarballs; \x5C\n" >> $df
# We are deleting the source directory becaues later (at 'docker run'
# time), the 'source' will be mounted directly from the host operating
# system.
printf " cd /home/maneager; \x5C\n" >> $df
printf " rm -rf source\n" >> $df
# Build the Maneage container and delete the temporary directory. The
# '--progress plain' option is for Docker to print all the outputs
# (otherwise, it will only print a very small part!).
cd $tmp_dir
docker build ./ -t $project_name \
--progress=plain \
--shm-size=$shm_size \
--no-cache \
2>&1 | tee build.log
cd ..
rm -rf $tmp_dir
fi
# If the user wants to save the container (into a file that does not
# exist), do it here. If the file exists, it will only be used for creating
# the container in the previous stages.
if ! [ x"$image_file" = x ] && ! [ -f "$image_file" ]; then
# Save the image into a tarball
tarname=$(echo $image_file | sed -e's|.gz$||')
if [ $quiet = 0 ]; then
printf "$scriptname: info: saving docker image to '$tarname'"
fi
docker save -o $tarname $project_name
# Compress the saved image
if [ $quiet = 0 ]; then
printf "$scriptname: info: compressing to '$image_file' (can "
printf "take +10 minutes, but volume decreases by more than half!)"
fi
gzip --best $tarname
fi
# If the user just wanted to build the base operating system, abort the
# script here.
if ! [ x"$build_only" = x ]; then
if [ $quiet = 0 ]; then
printf "$scriptname: info: Maneaged project has been configured "
printf "successfully in the '$project_name' image"
fi
exit 0
fi
# Run the analysis within the Maneage'd container
# -----------------------------------------------
#
# The startup command of the container is managed though the 'shellopt'
# variable that starts here.
shellopt=""
if [ $container_shell = 1 ] || [ $project_shell = 1 ]; then
# If the user wants to start the project shell within the container,
# add the necessary command.
if [ $project_shell = 1 ]; then
shellopt="/bin/bash -c 'cd source; ./project shell;'"
fi
# Finish the 'shellop' string with a single quote (necessary in any
# case) and run Docker.
interactiveopt="-it"
# No interactive shell requested, just run the project.
else
interactiveopt=""
shellopt="/bin/bash -c 'cd source; ./project make --jobs=$jobs;'"
fi
# Execute Docker. The 'eval' is because the 'shellopt' variable contains a
# single-quote that the shell should "evaluate".
eval docker run \
-v "$analysis_dir":/home/maneager/build/analysis \
-v "$source_dir":/home/maneager/source \
$input_dir_mnt \
$shm_mnt \
$interactiveopt \
$project_name \
$shellopt
|