#!/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 # # 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 . # 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 < /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 < $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