diff --git a/.ci/Jenkinsfile b/.ci/Jenkinsfile
index 3ed489980f749a9f275268e8611fddc35b1c3dbe..7f5fca536b5461d087f458e9f5718615bb1d4b2b 100755
--- a/.ci/Jenkinsfile
+++ b/.ci/Jenkinsfile
@@ -28,30 +28,21 @@ pipeline {
 	}
 
 	environment {
-		CONTAINER_STYLE = "visionary"
 		YASHCHIKI_HOST_ENV_PATH = "${WORKSPACE}/host.env"
 	}
 
 	stages {
 		stage('Container Build') {
+			// TODO: remove once unused
 			environment {
-				DOCKER_BASE_IMAGE = "debian:bullseye"
-				DEPENDENCY_PYTHON = "python@3.8.2"
+				CONTAINER_STYLE = "visionary"
 				YASHCHIKI_INSTALL = "${WORKSPACE}/yashchiki"
 				YASHCHIKI_META_DIR = "${WORKSPACE}/meta"
-				YASHCHIKI_RECIPE_PATH = "${WORKSPACE}/visionary_recipe.def"
 				YASHCHIKI_CACHES_ROOT = "${HOME}"
 				YASHCHIKI_SPACK_PATH = "${env.WORKSPACE}/spack"
-				YASHCHIKI_IMAGE_NAME = "singularity_visionary_temp.img"
 				YASHCHIKI_SANDBOXES = "sandboxes"
 				YASHCHIKI_PROXY_HTTP = "http://proxy.kip.uni-heidelberg.de:8080"
 				YASHCHIKI_PROXY_HTTPS = "http://proxy.kip.uni-heidelberg.de:8080"
-				YASHCHIKI_BUILD_SPACK_GCC = 1
-				YASHCHIKI_SPACK_GCC_VERSION = "11.2.0"
-				YASHCHIKI_SPACK_GCC = "gcc@${YASHCHIKI_SPACK_GCC_VERSION}"
-				TMPDIR = "/tmp/${env.NODE_NAME}"
-				JOB_TMP_SPACK = sh(script: "mkdir -p ${env.TMPDIR} &>/dev/null; mktemp -d ${env.TMPDIR}/spack-XXXXXXXXXX",
-				                   returnStdout: true).trim()
 				BUILD_CACHE_NAME = "${params.BUILD_CACHE_NAME}"  // propagate parameter to environment
 			}
 			stages {
@@ -60,7 +51,6 @@ pipeline {
 						script {
 							cleanupSteps()
 						}
-						sh "mkdir -p \"${env.JOB_TMP_SPACK}\" && chmod 777 \"${env.JOB_TMP_SPACK}\""
 					}
 				}
 				stage('yashchiki Checkout') {
@@ -77,59 +67,49 @@ pipeline {
 						}
 					}
 				}
-				stage('Validate environment') {
-					steps {
-						sh "bash lib/yashchiki/validate_environment.sh"
-					}
-				}
 				stage('Dump Meta Info') {
 					steps {
+						sh "mkdir -p ${YASHCHIKI_META_DIR}"
 						sh "bash bin/yashchiki_dump_meta_info.sh"
 						sh "bash bin/yashchiki_notify_gerrit.sh -m 'Build containing this change started..'"
 					}
 				}
-				stage('Spack Fetch') {
-					steps {
-						script {
-							try {
-								sh "bash lib/yashchiki/fetch.sh"
-							}
-							catch (Throwable t) {
-								archiveArtifacts "errors_concretization.log"
-								throw t
-							}
-							spec_folder_in_container = sh(script: "bash lib/yashchiki/get_host_env.sh SPEC_FOLDER_IN_CONTAINER", returnStdout: true).trim()
-							archiveArtifacts(artifacts: "sandboxes/*/$spec_folder_in_container/*.yaml", allowEmptyArchive: true)
-						}
-					}
-				}
 				stage('Deploy utilities') {
 					steps {
 						sh "bash bin/yashchiki_deploy_utilities.sh"
 					}
 				}
-				stage('Create visionary recipe') {
-					steps {
-						sh "bash share/yashchiki/styles/visionary/create_recipe.sh"
-					}
-				}
-				stage('Build sandbox') {
-					steps {
-						sh "bash lib/yashchiki/build_sandbox.sh"
-					}
-				}
 				stage('Build container image') {
 					steps {
-						sh "bash lib/yashchiki/build_image.sh"
+						script {
+							try {
+								sh "python3 bin/yashchiki visionary ${WORKSPACE}/spack singularity_visionary_temp.img " +
+								   "--log-dir=log " +
+								   "--proxy-http=${YASHCHIKI_PROXY_HTTP} " +
+								   "--proxy-https=${YASHCHIKI_PROXY_HTTPS} " +
+								   "--tmp-subdir=${env.NODE_NAME} " +
+								   "--meta-dir=${YASHCHIKI_META_DIR} " +
+								   "--caches-dir=${YASHCHIKI_CACHES_ROOT} " +
+								   "--sandboxes-dir=${YASHCHIKI_SANDBOXES} " +
+								   "--host-env-filename=${WORKSPACE}/host.env " +
+								   "--build-cache-name=${BUILD_CACHE_NAME} " +
+								   ("${CONTAINER_BUILD_TYPE}" == "stable" ? "--update-build-cache " : "") +
+								   "--recipe-filename=${WORKSPACE}/visionary_recipe.def"
+							} catch (Throwable t) {
+								archiveArtifacts "errors_concretization.log"
+								throw t
+							}
+							archiveArtifacts(artifacts: "sandboxes/*/opt/spack_specs/*.yaml", allowEmptyArchive: true)
+							archiveArtifacts(artifacts: "log/*.log", allowEmptyArchive: true)
+						}
 					}
 				}
-				stage('Update build cache and export container') {
+				stage('Export container') {
 					steps {
 						script {
 							// we only want the container name, tail everything else
 							CONTAINER_IMAGE = sh(script: "bash bin/yashchiki_deploy_container.sh | tail -n 1", returnStdout: true).trim()
 						}
-						sh "bash lib/yashchiki/update_build_cache.sh -c \"$CONTAINER_IMAGE\""
 						sh "bash bin/yashchiki_notify_gerrit.sh -t Build -c \"$CONTAINER_IMAGE\""
 					}
 				}
diff --git a/.ci/Jenkinsfile_asic b/.ci/Jenkinsfile_asic
index 118a6480e9a62d64c20f408dc481e375fa7b92df..b38dda96f482ceddcc7fb31234bda896ecd52b18 100755
--- a/.ci/Jenkinsfile_asic
+++ b/.ci/Jenkinsfile_asic
@@ -28,41 +28,30 @@ pipeline {
 	}
 
 	environment {
-		CONTAINER_STYLE = "asic"
 		YASHCHIKI_HOST_ENV_PATH = "${WORKSPACE}/host.env"
 	}
 
 	stages {
 		stage('Container Build') {
-			agent { label 'conviz1||conviz2' }
+			// TODO: remove once unused
 			environment {
-				DOCKER_BASE_IMAGE = "centos:7"
-				// versions from system packages
-				DEPENDENCY_PYTHON = "python@3.8.3"
+				CONTAINER_STYLE = "asic"
 				YASHCHIKI_INSTALL = "${WORKSPACE}/yashchiki"
 				YASHCHIKI_META_DIR = "${WORKSPACE}/meta"
-				YASHCHIKI_RECIPE_PATH = "${WORKSPACE}/asic_recipe.def"
 				YASHCHIKI_CACHES_ROOT = "${HOME}"
 				YASHCHIKI_SPACK_PATH = "${env.WORKSPACE}/spack"
-				YASHCHIKI_IMAGE_NAME = "singularity_asic_temp.img"
 				YASHCHIKI_SANDBOXES = "sandboxes"
 				YASHCHIKI_PROXY_HTTP = "http://proxy.kip.uni-heidelberg.de:8080"
 				YASHCHIKI_PROXY_HTTPS = "http://proxy.kip.uni-heidelberg.de:8080"
-				YASHCHIKI_BUILD_SPACK_GCC = 0
-				YASHCHIKI_SPACK_GCC_VERSION = "4.8.5"
-				YASHCHIKI_SPACK_GCC = "gcc@${YASHCHIKI_SPACK_GCC_VERSION}"
-				TMPDIR = "/tmp/${env.NODE_NAME}"
-				JOB_TMP_SPACK = sh(script: "mkdir -p ${env.TMPDIR} &>/dev/null; mktemp -d ${env.TMPDIR}/spack-XXXXXXXXXX",
-				                   returnStdout: true).trim()
 				BUILD_CACHE_NAME = "${params.BUILD_CACHE_NAME}"  // propagate parameter to environment
 			}
+			agent { label 'conviz1||conviz2' }
 			stages {
 				stage('Pre-build Cleanup') {
 					steps {
 						script {
 							cleanupSteps()
 						}
-						sh "mkdir -p \"${env.JOB_TMP_SPACK}\" && chmod 777 \"${env.JOB_TMP_SPACK}\""
 					}
 				}
 				stage('yashchiki Checkout') {
@@ -79,59 +68,49 @@ pipeline {
 						}
 					}
 				}
-				stage('Validate environment') {
+				stage('Deploy utilities') {
 					steps {
-						sh "bash lib/yashchiki/validate_environment.sh"
+						sh "bash bin/yashchiki_deploy_utilities.sh"
 					}
 				}
 				stage('Dump Meta Info') {
 					steps {
+						sh "mkdir -p ${YASHCHIKI_META_DIR}"
 						sh "bash bin/yashchiki_dump_meta_info.sh"
 						sh "bash bin/yashchiki_notify_gerrit.sh -m 'Build containing this change started..'"
 					}
 				}
-				stage('Spack Fetch') {
+				stage('Build container image') {
 					steps {
 						script {
 							try {
-								sh "bash lib/yashchiki/fetch.sh"
-							}
-							catch (Throwable t) {
+								sh "python3 bin/yashchiki visionary ${WORKSPACE}/spack singularity_asic_temp.img " +
+								   "--log-dir=log " +
+								   "--proxy-${YASHCHIKI_PROXY_HTTP} " +
+								   "--proxy-https=${YASHCHIKI_PROXY_HTTPS} " +
+								   "--tmp-subdir=${env.NODE_NAME} " +
+								   "--meta-dir=${YASHCHIKI_META_DIR} " +
+								   "--caches-dir=${YASHCHIKI_CACHES_ROOT} " +
+								   "--build-cache-name=${BUILD_CACHE_NAME} " +
+								   "--sandboxes-dir=${YASHCHIKI_SANDBOXES} " +
+								   "--host-env-filename=${WORKSPACE}/host.env " +
+								   ("${CONTAINER_BUILD_TYPE}" == "stable" ? "--update-build-cache " : "") +
+								   "--recipe-filename=${WORKSPACE}/asic_recipe.def "
+							} catch (Throwable t) {
 								archiveArtifacts "errors_concretization.log"
 								throw t
 							}
-							spec_folder_in_container = sh(script: "lib/yashchiki/get_host_env.sh SPEC_FOLDER_IN_CONTAINER", returnStdout: true).trim()
-							archiveArtifacts(artifacts: "sandboxes/*/$spec_folder_in_container/*.yaml", allowEmptyArchive: true)
+							archiveArtifacts(artifacts: "sandboxes/*/opt/spack_specs/*.yaml", allowEmptyArchive: true)
+							archiveArtifacts(artifacts: "log/*.log", allowEmptyArchive: true)
 						}
 					}
 				}
-				stage('Deploy utilities') {
-					steps {
-						sh "bash bin/yashchiki_deploy_utilities.sh"
-					}
-				}
-				stage('Create asic recipe') {
-					steps {
-						sh "share/yashchiki/styles/asic/create_recipe.sh"
-					}
-				}
-				stage('Build sandbox') {
-					steps {
-						sh "bash lib/yashchiki/build_sandbox.sh"
-					}
-				}
-				stage('Build container image') {
-					steps {
-						sh "bash lib/yashchiki/build_image.sh"
-					}
-				}
-				stage('Update build cache and export container') {
+				stage('Export container') {
 					steps {
 						script {
 							// we only want the container name, tail everything else
 							CONTAINER_IMAGE = sh(script: "bin/yashchiki_deploy_container.sh | tail -n 1", returnStdout: true).trim()
 						}
-						sh "bash lib/yashchiki/update_build_cache.sh -c \"$CONTAINER_IMAGE\""
 						sh "bash bin/yashchiki_notify_gerrit.sh -t Build -c \"$CONTAINER_IMAGE\""
 					}
 				}
diff --git a/bin/yashchiki b/bin/yashchiki
new file mode 100644
index 0000000000000000000000000000000000000000..10446bd641b0b31acb9a4ed786c443867e512063
--- /dev/null
+++ b/bin/yashchiki
@@ -0,0 +1,248 @@
+#!/usr/bin/env python
+
+import argparse
+import os
+import pathlib
+import subprocess
+import tempfile
+import textwrap
+from typing import Optional
+
+
+def check_no_globbing(path: Optional[str]) -> None:
+    """
+    Check that no globbing characters are used in the given path.
+
+    :param path: Path to check.
+    :raises ContainsGlobError: If glob cahracters are used in path.
+    """
+    globs = ["*", "?", "[", "]", "$", "{", "}", "|"]
+
+    class ContainsGlobError(RuntimeError):
+        pass
+
+    # note: str(None) is 'None' and is therefore handled correctly
+    if any(glob in str(path) for glob in globs):
+        raise ContainsGlobError(f"Path {path} containing any of "
+                                f"{' '.join(globs)} is not supported.")
+
+
+class HelpFormatter(
+        argparse.RawDescriptionHelpFormatter,
+        argparse.ArgumentDefaultsHelpFormatter):
+    """
+    Formatting for argument parser help message generation.
+    """
+
+
+parser = argparse.ArgumentParser(
+    prog="yashchiki",
+    formatter_class=HelpFormatter,
+    description=textwrap.dedent("""\
+        Yashchiki singularity image builder.
+
+        For a successful image build, a style of container to build, a spack
+        installation and a name for the resulting image is required.
+
+        Read: yashchiki builds a container of STYLE with spack in SPACK_DIR
+              to OUTPUT.
+    """))
+
+# mandatory
+parser.add_argument(
+    "style", type=str, choices=["visionary", "asic"],
+    help="Style of container to build.")
+parser.add_argument(
+    "spack_dir", type=pathlib.Path,
+    help="Location of spack to use.")
+parser.add_argument(
+    "output", type=pathlib.Path,
+    help="File name of the resulting container image.")
+# optional but important
+parser.add_argument(
+    "--update-build-cache", action="store_true",
+    help="Update build cache.")
+# optional with persistent default
+parser.add_argument(
+    "--caches-dir", type=pathlib.Path,
+    default=os.path.expanduser("~/.yashchiki/"),
+    help="Location of caches to use.")
+parser.add_argument(
+    "--log-dir", type=pathlib.Path,
+    default=os.path.expanduser("~/.yashchiki/log/"),
+    help="Location of logs to use.")
+parser.add_argument(
+    "--sandboxes-dir", type=pathlib.Path,
+    default=os.path.expanduser("~/.yashchiki/sandboxes"),
+    help="Location of sandboxes for container creation to use.")
+# optional with temporary default
+parser.add_argument(
+    "--meta-dir", type=pathlib.Path,
+    help="Folder where to store meta information to be copied into the "
+         "container. If not provided, a temporary directory is used.")
+parser.add_argument(
+    "--host-env-filename", type=pathlib.Path,
+    help="Location of host environment storage file to use within container "
+         "build. If not provided, a temporary location is used.")
+parser.add_argument(
+    "--tmp-subdir", type=pathlib.Path, default="",
+    help=f"Directory under {tempfile.gettempdir()} which to use as root for "
+         "temporary files to be owned by spack.")
+parser.add_argument(
+    "--recipe-filename", type=pathlib.Path,
+    help=f"Explicit filename for singularity recipe to construct. If not "
+         "provided, a temporary location is used.")
+parser.add_argument(
+    "--build-cache-name", type=str, default="default",
+    help="Name of build cache to use, resides under "
+         "<CACHES_DIR>/build_caches/<BUILD_CACHE_NAME>.")
+
+# optional options
+parser.add_argument(
+    "--proxy-http", type=str,
+    help="HTTP proxy to use when required.")
+parser.add_argument(
+    "--proxy-https", type=str,
+    help="HTTPS proxy to use when required.")
+parser.add_argument(
+    "--debug", action="store_true",
+    help="Enable debug-level logging.")
+
+args = parser.parse_args()
+
+# yashchiki program root directory to use for script location
+root_dir = pathlib.Path(__file__).parent.parent
+
+# check provided paths
+if not args.spack_dir.is_dir():
+    raise NotADirectoryError("spack_dir is required to be a path to an "
+                             "existing directory.")
+if (args.meta_dir is not None) and (not args.meta_dir.is_dir()):
+    raise NotADirectoryError("meta-dir is required to be a path to an "
+                             "existing directory.")
+
+paths = [
+    args.spack_dir,
+    args.output,
+    args.caches_dir,
+    args.log_dir,
+    args.sandboxes_dir,
+    args.meta_dir,
+    args.host_env_filename,
+    args.tmp_subdir,
+    args.recipe_filename
+]
+# ensure no globbing is performed in the paths for shell scripts to work
+for path_to_check in paths:
+    check_no_globbing(path_to_check)
+
+# collection of environment variables used to configure the shell scripts'
+# behavior
+env = {
+    "DOCKER_BASE_IMAGE": "debian:bullseye" if args.style == "visionary" else "centos:7",
+    # This needs to be here because otherwise the default python
+    # (2.7.18) will pollute the spec and lead to a conflict
+    # can be removed as soon as the explicit preferred version
+    # is dropped
+    "DEPENDENCY_PYTHON": "python@3." + ("8.2" if args.style == "visionary" else "6.8"),
+    "YASHCHIKI_BUILD_SPACK_GCC": "1" if args.style == "visionary" else "0",
+    "YASHCHIKI_SPACK_GCC_VERSION": "11.2.0" if args.style == "visionary" else "4.8.5",
+    "YASHCHIKI_SPACK_GCC": "gcc@11.2.0" if args.style == "visionary" else "gcc@4.8.5",
+    "WORKSPACE": os.getcwd(), # FIXME: should not be required
+    "CONTAINER_STYLE": args.style,
+    "CONTAINER_BUILD_TYPE": "testing", # FIXME: should not be required
+    "YASHCHIKI_DEBUG": str(int(args.debug)),
+    "YASHCHIKI_SANDBOXES": args.sandboxes_dir,
+    "YASHCHIKI_IMAGE_NAME": args.output,
+    "YASHCHIKI_SPACK_PATH": args.spack_dir,
+    "YASHCHIKI_BUILD_CACHE_NAME": args.build_cache_name,
+    "TMPDIR": os.path.join(tempfile.gettempdir(), args.tmp_subdir),
+    "YASHCHIKI_CACHES_ROOT": args.caches_dir,
+} | os.environ
+
+
+# optionally forward http{,s} proxy if argument given
+if args.proxy_http:
+    env = env | {"YASHCHIKI_PROXY_HTTP": args.proxy_http}
+
+if args.proxy_https:
+    env = env | {"YASHCHIKI_PROXY_HTTPS": args.proxy_https}
+
+# create directory for logs
+args.log_dir.mkdir(parents=True, exist_ok=True)
+
+pathlib.Path(env["TMPDIR"]).mkdir(exist_ok=True, parents=True)
+
+
+def run(script: str, env: dict, script_args: list = []):
+    """
+    Execute the given script.
+
+    :param script: Script to execute.
+    :param env: Enviroment to use for execution.
+    :param script_args: Arguments to supply to the script.
+    """
+    stdout = ""
+    try:
+        if args.debug:
+            print(f"executing: {script} {script_args}")
+        out = subprocess.run(
+            ["bash", os.path.join(root_dir, script)] + script_args,
+            env=env, check=True, stdout=subprocess.PIPE,
+            stderr=subprocess.STDOUT, encoding="utf-8")
+        stdout = out.stdout
+        if args.debug:
+            print(stdout)
+    except subprocess.CalledProcessError as error:
+        stdout = error.stdout
+        print(stdout)
+        with args.log_dir.joinpath(
+                script.replace("/", "_") + ".log").open("w+") as file:
+            file.write(stdout)
+        raise
+    else:
+        with args.log_dir.joinpath(
+                script.replace("/", "_") + ".log").open("w+") as file:
+            file.write(stdout)
+
+
+with tempfile.TemporaryDirectory(prefix="spack-", dir=env["TMPDIR"]) \
+        as temporary_directory_spack, \
+        tempfile.TemporaryDirectory() as temporary_directory:
+    temporary_directory = pathlib.Path(temporary_directory)
+
+    env = env | {"JOB_TMP_SPACK": temporary_directory_spack}
+
+    # singularity recipe filename defaults to temporary file
+    if args.recipe_filename is not None:
+        recipe_filename = args.recipe_filename
+    else:
+        recipe_filename = temporary_directory.joinpath("recipe.def")
+    env = env | {"YASHCHIKI_RECIPE_PATH": recipe_filename}
+
+    # meta data directory defaults to temporary folder
+    if args.meta_dir is not None:
+        meta_dir = args.meta_dir
+    else:
+        meta_dir = temporary_directory.joinpath("meta")
+    env = env | {"YASHCHIKI_META_DIR": meta_dir}
+
+    # host environment storage filename defaults to temporary file
+    if args.host_env_filename is not None:
+        host_env_filename = args.host_env_filename
+    else:
+        host_env_filename = temporary_directory.joinpath("host.env")
+    env = env | {"YASHCHIKI_HOST_ENV_PATH": host_env_filename}
+
+    run("lib/yashchiki/validate_environment.sh", env)
+    run("lib/yashchiki/create_spack_user.sh", env)
+    run("lib/yashchiki/create_caches.sh", env)
+    run("lib/yashchiki/fetch.sh", env)
+    run(str(pathlib.Path("share", "yashchiki", "styles", args.style,
+                         "create_recipe.sh")),
+        env)
+    run("lib/yashchiki/build_sandbox.sh", env)
+    run("lib/yashchiki/build_image.sh", env)
+    if args.update_build_cache:
+        run("lib/yashchiki/update_build_cache.sh", env, ["-c", args.output])
+    run("lib/yashchiki/restore_host_user_ownership.sh", env)
diff --git a/bin/yashchiki_dump_meta_info.sh b/bin/yashchiki_dump_meta_info.sh
index 1eb17e9052a1e76eb5000cd7c4f1f07b8ac2f09b..50eea1f64b1848afd5fda31b6c0efa744127a96d 100755
--- a/bin/yashchiki_dump_meta_info.sh
+++ b/bin/yashchiki_dump_meta_info.sh
@@ -7,24 +7,26 @@ set -Eeuo pipefail
 shopt -s inherit_errexit
 
 ROOT_DIR="$(dirname "$(dirname "$(readlink -m "${BASH_SOURCE[0]}")")")"
-source "${ROOT_DIR}/lib/yashchiki/commons.sh"
+source "${ROOT_DIR}/lib/yashchiki/gerrit.sh"
 
-mkdir -p "${META_DIR_OUTSIDE}"
+mkdir -p "${YASHCHIKI_META_DIR}"
 
 (
-    cd "${YASHCHIKI_INSTALL}"
-    git log > "${META_DIR_OUTSIDE}/yashchiki_git.log"
-    if [ "${CONTAINER_BUILD_TYPE}" = "testing" ]; then
-        gerrit_get_current_change_commits \
-            > "${META_DIR_OUTSIDE}/current_changes-yashchiki.dat"
+    if [ -n "${YASHCHIKI_INSTALL:-}" ]; then
+        cd "${YASHCHIKI_INSTALL}"
+        git log > "${YASHCHIKI_META_DIR}/yashchiki_git.log"
+        if [ "${CONTAINER_BUILD_TYPE}" = "testing" ]; then
+            gerrit_get_current_change_commits \
+                > "${YASHCHIKI_META_DIR}/current_changes-yashchiki.dat"
+        fi
     fi
 )
 
 (
     cd ${YASHCHIKI_SPACK_PATH}
-    git log > "${META_DIR_OUTSIDE}/spack_git.log"
+    git log > "${YASHCHIKI_META_DIR}/spack_git.log"
     if [ "${CONTAINER_BUILD_TYPE}" = "testing" ]; then
         gerrit_get_current_change_commits \
-            > "${META_DIR_OUTSIDE}/current_changes-spack.dat"
+            > "${YASHCHIKI_META_DIR}/current_changes-spack.dat"
     fi
 )
diff --git a/lib/yashchiki/build_image.sh b/lib/yashchiki/build_image.sh
index 45cffc6bd65731b46e00795a5bcdd3618244193d..c07fc66287a0980084cc76371d291544f1b4cd7e 100755
--- a/lib/yashchiki/build_image.sh
+++ b/lib/yashchiki/build_image.sh
@@ -14,6 +14,11 @@ TARGET_FOLDER="$(find ${YASHCHIKI_SANDBOXES} -mindepth 1 -maxdepth 1)"
 # -> it needs to be bind mounted to the sandbox folder
 sudo mount --bind "${YASHCHIKI_SPACK_PATH}" "${TARGET_FOLDER}/opt/spack"
 
+if test -f "${YASHCHIKI_IMAGE_NAME}"; then
+    echo "Image at ${YASHCHIKI_IMAGE_NAME} exists."
+    exit 1
+fi
+
 # TODO: singularity 3.1 produces SIF w/o setuid flags on files, using a newer
 # singularity for the image build
 #sudo singularity build ${YASHCHIKI_IMAGE_NAME} "${TARGET_FOLDER}"
diff --git a/lib/yashchiki/commons.sh b/lib/yashchiki/commons.sh
index d6144c04df537c0fbabf41daeeac8b258632d03b..5afdceafa72afa974874a29075520689edda0770 100755
--- a/lib/yashchiki/commons.sh
+++ b/lib/yashchiki/commons.sh
@@ -122,7 +122,7 @@ export PRESERVED_PACKAGES_INSIDE
 export PRESERVED_PACKAGES_OUTSIDE
 
 META_DIR_INSIDE="/opt/meta"
-META_DIR_OUTSIDE="$(get_host_env YASHCHIKI_META_DIR)${META_DIR_INSIDE}"
+META_DIR_OUTSIDE="$(get_host_env YASHCHIKI_META_DIR)"
 export META_DIR_INSIDE
 export META_DIR_OUTSIDE
 
diff --git a/lib/yashchiki/create_caches.sh b/lib/yashchiki/create_caches.sh
new file mode 100755
index 0000000000000000000000000000000000000000..88180700d04b9937f61b02f083c058209d15aabc
--- /dev/null
+++ b/lib/yashchiki/create_caches.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+set -euo pipefail
+shopt -s inherit_errexit 2>/dev/null || true
+
+if [ ! -d "${YASHCHIKI_CACHES_ROOT}" ]; then
+    mkdir -p "${YASHCHIKI_CACHES_ROOT}"
+fi
+
+if [ ! -d "${YASHCHIKI_CACHES_ROOT}/build_caches" ]; then
+    mkdir -p "${YASHCHIKI_CACHES_ROOT}/build_caches"
+fi
+
+if [ ! -d "${YASHCHIKI_CACHES_ROOT}/download_cache" ]; then
+    mkdir -p "${YASHCHIKI_CACHES_ROOT}/download_cache"
+fi
+
+if [ ! -d "${YASHCHIKI_CACHES_ROOT}/spack_ccache" ]; then
+    mkdir -p "${YASHCHIKI_CACHES_ROOT}/spack_ccache"
+fi
+
+if [ ! -d "${YASHCHIKI_CACHES_ROOT}/preserved_packages" ]; then
+    mkdir -p "${YASHCHIKI_CACHES_ROOT}/preserved_packages"
+fi
+
+# spack requires ccache and preserved packages to be accessible within the container
+sudo chown -R spack:nogroup "${YASHCHIKI_CACHES_ROOT}/spack_ccache"
+sudo chown -R spack:nogroup "${YASHCHIKI_CACHES_ROOT}/preserved_packages"
diff --git a/lib/yashchiki/create_spack_user.sh b/lib/yashchiki/create_spack_user.sh
new file mode 100755
index 0000000000000000000000000000000000000000..2807fd1b3d1e4b3f1d98e9877b4c630154903a1e
--- /dev/null
+++ b/lib/yashchiki/create_spack_user.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+set -euo pipefail
+shopt -s inherit_errexit 2>/dev/null || true
+
+# we need the spack user outside of the container, create it here if it is not present already
+if [ id spack &>/dev/null ]; then
+	sudo useradd spack --uid 888 --no-create-home --system --shell /bin/bash
+fi
diff --git a/lib/yashchiki/gerrit.sh b/lib/yashchiki/gerrit.sh
new file mode 100755
index 0000000000000000000000000000000000000000..aeccdb6ff588d960581f5e28e7e47ffc1b5bd331
--- /dev/null
+++ b/lib/yashchiki/gerrit.sh
@@ -0,0 +1,105 @@
+#!/bin/bash
+
+set -euo pipefail
+shopt -s inherit_errexit 2>/dev/null || true
+
+# Get gerrit username
+gerrit_username() {
+    echo "${GERRIT_USERNAME:-hudson}"
+}
+
+# Read the current gerrit config from `.gitreview` into global variables:
+# * gerrit_branch
+# * gerrit_remote
+# * gerrit_host
+# * gerrit_port
+# * gerrit_project
+#
+# Unfortunately, since we cannot return values from function, they have to be
+# global variables.
+gerrit_read_config() {
+    local git_dir
+    git_dir="$(git rev-parse --show-toplevel)"
+    # remote branch
+    gerrit_branch="$(grep "^defaultbranch=" "${git_dir}/.gitreview" | cut -d = -f 2)"
+    gerrit_remote="$(grep "^defaultremote=" "${git_dir}/.gitreview" | cut -d = -f 2)"
+    gerrit_host="$(grep "^host=" "${git_dir}/.gitreview" | cut -d = -f 2)"
+    gerrit_port="$(grep "^port=" "${git_dir}/.gitreview" | cut -d = -f 2)"
+    gerrit_project="$(grep "^project=" "${git_dir}/.gitreview" | cut -d = -f 2)"
+}
+
+# Ensure that the gerrit remote is properly set up in the current git directory.
+gerrit_ensure_setup() {
+    gerrit_read_config
+
+    if ! git remote | grep -q "${gerrit_remote}"; then
+        # ensure git review is set up
+        git remote add "${gerrit_remote}" "ssh://$(gerrit_username)@${gerrit_host}:${gerrit_port}/${gerrit_project}"
+    fi
+    git fetch "${gerrit_remote}" "${gerrit_branch}"
+}
+
+gerrit_filter_current_change_commits() {
+    awk '$1 ~ /^commit$/ { commit=$2 }; $1 ~ /^Change-Id:/ { print commit }'
+}
+
+# Get the current stack of changesets in the current git repo as commit ids.
+gerrit_get_current_change_commits() {
+    gerrit_ensure_setup
+
+    # only provide change-ids that are actually present in gerrit
+    comm -1 -2 \
+        <(git log "${gerrit_remote}/${gerrit_branch}..HEAD" \
+            | gerrit_filter_current_change_commits | sort) \
+        <(git ls-remote "${gerrit_remote}" | awk '$2 ~ /^refs\/changes/ { print $1 }' | sort)
+}
+
+# Convenience method to print the ssh command necessary to connect to gerrit.
+#
+# Note: Make sure the gerrit config was read prior to calling this!
+gerrit_cmd_ssh() {
+    echo -n "ssh -p ${gerrit_port} $(gerrit_username)@${gerrit_host} gerrit"
+}
+
+# Post comment on the given change-id
+#
+# Gerrit host/post will be read from current git repository.
+#
+# Args:
+#   -c <change>
+#   -m <message>
+gerrit_notify_change() {
+    local change=""
+    local message=""
+    local verified=""
+    local opts OPTIND OPTARG
+    while getopts ":c:m:v:" opts; do
+        case "${opts}" in
+            c)  change="${OPTARG}"
+                ;;
+            m)  message="${OPTARG}"
+                ;;
+            v)  verified="${OPTARG}"
+                ;;
+            *)
+                echo "Invalid argument: ${opts}" >&2
+                return 1
+                ;;
+        esac
+    done
+    shift $(( OPTIND - 1 ))
+
+    if [ -z "${change}" ]; then
+        echo "ERROR: No change to post to given!" >&2
+        return 1
+    fi
+    if [ -z "${message}" ]; then
+        echo "ERROR: No message given!" >&2
+        return 1
+    fi
+
+    gerrit_read_config
+    $(gerrit_cmd_ssh) review --message "\"${message}\"" \
+        "$([ -n "${verified}" ] && echo --verified "${verified}")" \
+        "${change}"
+}
diff --git a/lib/yashchiki/restore_host_user_ownership.sh b/lib/yashchiki/restore_host_user_ownership.sh
new file mode 100755
index 0000000000000000000000000000000000000000..d3230ddb4fa3ce761343dbbe8ce785713521da78
--- /dev/null
+++ b/lib/yashchiki/restore_host_user_ownership.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+set -euo pipefail
+shopt -s inherit_errexit 2>/dev/null || true
+
+if [ -d "${YASHCHIKI_SPACK_PATH}" ]; then
+	sudo chown -R $(id -un):$(id -gn) "${YASHCHIKI_SPACK_PATH}"
+fi
+
+if [ -d "${JOB_TMP_SPACK}" ]; then
+	sudo chown -R $(id -un):$(id -gn) "${JOB_TMP_SPACK}"
+fi
diff --git a/lib/yashchiki/update_build_cache.sh b/lib/yashchiki/update_build_cache.sh
index bac589990735aeea413306a374d919950823befc..49ff245877e418c00b9ba7550189ffbf63dfd709 100755
--- a/lib/yashchiki/update_build_cache.sh
+++ b/lib/yashchiki/update_build_cache.sh
@@ -3,12 +3,6 @@
 set -euo pipefail
 shopt -s inherit_errexit 2>/dev/null || true
 
-# only update build cache for stable builds
-if [ "${CONTAINER_BUILD_TYPE:-}" != "stable" ]; then
-    echo "Not updating build cache for testing builds." 1>&2
-    exit 0
-fi
-
 usage() { echo "Usage: ${0} -c <container>" 1>&2; exit 1; }
 
 while getopts ":c:" opts; do