#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" IMAGE_NAME="${IMAGE_NAME:-migration-pipeline:ubuntu26.04}" CONTAINER_USER="${CONTAINER_USER:-${USER:-runner}}" CONTAINER_GROUP="${CONTAINER_GROUP:-$CONTAINER_USER}" ENABLE_CONTAINER_SUDO="${ENABLE_CONTAINER_SUDO:-1}" QT_ROOT_DEFAULT="${QT_ROOT_DEFAULT:-/opt/Qt/6.8.3/gcc_64}" GIT_REPO_DIR="" WORKDIR_IN_CONTAINER="" RUN_QT_SMOKE_TEST=0 RUN_RPM=0 PROJECT_RPM="" OS_VERSION_RPM="ubuntu24" LTS_VERSION_RPM="1.2.0" GUEZ_MODE_RPM="ON" QT_ROOT_OVERRIDE="" DOCKER_ARGS=() CMD_ARGS=() usage() { cat <<'EOF' Usage: ./run_docker.sh [options] [repo_dir] [-- command args...] Options: --no-sudo Disable passwordless sudo for the container user --workdir PATH Set working directory inside container --qt-smoke-test Run a Qt/CMake smoke test inside the container --qt-path PATH Override Qt root path inside container -h, --help Show this help Examples: ./run_docker.sh ./run_docker.sh /path/to/repo ./run_docker.sh /path/to/repo -- bash ./run_docker.sh --qt-smoke-test /path/to/repo ./run_docker.sh --qt-path /opt/Qt/6.8.3/gcc_64 /path/to/repo -- bash EOF } while [[ $# -gt 0 ]]; do case "$1" in --no-sudo) ENABLE_CONTAINER_SUDO=0 shift ;; --workdir) WORKDIR_IN_CONTAINER="$2" shift 2 ;; --qt-smoke-test) RUN_QT_SMOKE_TEST=1 shift ;; --qt-path) QT_ROOT_OVERRIDE="$2" shift 2 ;; --run-rpm) RUN_RPM=1 shift ;; --rpm-project) PROJECT_RPM="$2" shift 2 ;; --rpm-os) OS_VERSION_RPM="$2" shift 2 ;; --rpm-lts) LTS_VERSION_RPM="$2" shift 2 ;; --rpm-guez) GUEZ_MODE_RPM="$2" shift 2 ;; -h|--help) usage exit 0 ;; --) shift CMD_ARGS=("$@") break ;; -*) echo "Unknown option: $1" >&2 usage >&2 exit 1 ;; *) if [[ -z "$GIT_REPO_DIR" ]]; then GIT_REPO_DIR="$1" else CMD_ARGS+=("$1") fi shift ;; esac done HOST_UID="$(id -u)" HOST_GID="$(id -g)" add_optional_mount() { local host_path="$1" local container_path="$2" if [[ -e "$host_path" ]]; then DOCKER_ARGS+=( -v "$host_path:$container_path" ) fi } add_optional_mount "$HOME/.gitconfig" "/home/$CONTAINER_USER/.gitconfig" add_optional_mount "$HOME/.ssh" "/home/$CONTAINER_USER/.ssh" add_optional_mount "$HOME/.bashrc" "/home/$CONTAINER_USER/.bashrc" add_optional_mount "$HOME/.bash_aliases" "/home/$CONTAINER_USER/.bash_aliases" if [[ -n "$GIT_REPO_DIR" ]]; then if [[ ! -d "$GIT_REPO_DIR" ]]; then echo "Repository directory does not exist: $GIT_REPO_DIR" >&2 exit 1 fi GIT_REPO_DIR="$(cd "$GIT_REPO_DIR" && pwd)" DOCKER_ARGS+=( -v "$GIT_REPO_DIR:/repo" ) if [[ -d "$GIT_REPO_DIR/src" ]]; then DOCKER_ARGS+=( -v "$GIT_REPO_DIR/src:/src" ) fi # Prefer ./externals (plural) as requested; fall back to ./external if [[ -d "$GIT_REPO_DIR/externals" ]]; then DOCKER_ARGS+=( -v "$GIT_REPO_DIR/externals:/repo/external" ) echo "Mounting repo externals from $GIT_REPO_DIR/externals -> /repo/external" elif [[ -d "$GIT_REPO_DIR/external" ]]; then DOCKER_ARGS+=( -v "$GIT_REPO_DIR/external:/repo/external" ) echo "Mounting repo external from $GIT_REPO_DIR/external -> /repo/external" fi if [[ -z "$WORKDIR_IN_CONTAINER" ]]; then WORKDIR_IN_CONTAINER="/repo" fi fi if [[ -z "$WORKDIR_IN_CONTAINER" ]]; then WORKDIR_IN_CONTAINER="/app" fi if [[ -z "$QT_ROOT_OVERRIDE" ]]; then QT_ROOT_OVERRIDE="$QT_ROOT_DEFAULT" fi # Name containers with a readable timestamped name so they can be inspected if orphaned CONTAINER_NAME="${CONTAINER_NAME:-migration-pipeline-$(date +%s)-$HOST_UID}" DOCKER_NAME_FLAG="--name ${CONTAINER_NAME}" echo "Launching container with name: ${CONTAINER_NAME}" if [[ ! -f "$SCRIPT_DIR/.env" ]]; then echo "Missing env file: $SCRIPT_DIR/.env" >&2 exit 1 fi docker build -t "$IMAGE_NAME" "$SCRIPT_DIR" if [[ $RUN_QT_SMOKE_TEST -eq 1 ]]; then CMD_ARGS=( bash -lc " set -euo pipefail echo '== User ==' whoami echo '== Qt path ==' echo \"$QT_ROOT_OVERRIDE\" test -d \"$QT_ROOT_OVERRIDE\" echo '== CMake version ==' cmake --version echo '== Qt config ==' find \"$QT_ROOT_OVERRIDE\" -maxdepth 3 \\( -name 'Qt6Config.cmake' -o -name 'Qt6CoreConfig.cmake' \\) | sort TMPDIR=\$(mktemp -d) cat > \"\$TMPDIR/CMakeLists.txt\" <<'EOF' cmake_minimum_required(VERSION 3.21) project(QtSmokeTest LANGUAGES CXX) find_package(Qt6 REQUIRED COMPONENTS Core) add_executable(qt_smoke main.cpp) target_link_libraries(qt_smoke PRIVATE Qt6::Core) EOF cat > \"\$TMPDIR/main.cpp\" <<'EOF' #include #include #include int main(int argc, char **argv) { QCoreApplication app(argc, argv); std::cout << QString(\"Qt smoke test OK\").toStdString() << std::endl; return 0; } EOF cmake -S \"\$TMPDIR\" -B \"\$TMPDIR/build\" -G Ninja -DCMAKE_PREFIX_PATH=\"$QT_ROOT_OVERRIDE\" cmake --build \"\$TMPDIR/build\" \"\$TMPDIR/build/qt_smoke\" " ) fi if [[ ${RUN_RPM} -eq 1 ]]; then # Run rpm prepare script inside container if [[ -n "$PROJECT_RPM" ]]; then CMD_ARGS=(bash -lc "/app/prepare_rpm.sh '$PROJECT_RPM' '$OS_VERSION_RPM' '$LTS_VERSION_RPM' '$GUEZ_MODE_RPM'") else CMD_ARGS=(bash -lc "/app/prepare_rpm.sh") fi elif [[ ${#CMD_ARGS[@]} -eq 0 ]]; then CMD_ARGS=(bash) fi if [ -t 1 ]; then DOCKER_TTY_FLAG="-it" else DOCKER_TTY_FLAG="-i" fi docker run --rm $DOCKER_TTY_FLAG $DOCKER_NAME_FLAG \ --user root \ --env-file "$SCRIPT_DIR/.env" \ -v "$SCRIPT_DIR:/app" \ "${DOCKER_ARGS[@]}" \ -w "$WORKDIR_IN_CONTAINER" \ -e LOCAL_UID="$HOST_UID" \ -e LOCAL_GID="$HOST_GID" \ -e LOCAL_USER="$CONTAINER_USER" \ -e LOCAL_GROUP="$CONTAINER_GROUP" \ -e ENABLE_CONTAINER_SUDO="$ENABLE_CONTAINER_SUDO" \ -e WORKDIR_IN_CONTAINER="$WORKDIR_IN_CONTAINER" \ -e QT_ROOT="$QT_ROOT_OVERRIDE" \ -e Qt6_ROOT="$QT_ROOT_OVERRIDE" \ -e EXTERNAL_DIR="/repo/external" \ -e CMAKE_PREFIX_PATH="$QT_ROOT_OVERRIDE:/repo/external" \ -e PROJECT_BASE_DIR="${PROJECT_BASE_DIR:-./}" \ "$IMAGE_NAME" \ bash -s -- "${CMD_ARGS[@]}" <<'DOCKER_SCRIPT' set -euo pipefail ensure_group() { # If a group with the requested GID exists, use that group name. if getent group "${LOCAL_GID}" >/dev/null 2>&1; then getent group "${LOCAL_GID}" | cut -d: -f1 return fi # If the desired group name exists, try to change its GID if unused. if getent group "${LOCAL_GROUP}" >/dev/null 2>&1; then existing_gid=$(getent group "${LOCAL_GROUP}" | cut -d: -f3) if [[ "${existing_gid}" != "${LOCAL_GID}" ]]; then if ! getent group "${LOCAL_GID}" >/dev/null 2>&1; then groupmod -g "${LOCAL_GID}" "${LOCAL_GROUP}" || true else echo "Warning: desired GID ${LOCAL_GID} already in use by another group; keeping ${LOCAL_GROUP} gid ${existing_gid}" >&2 fi fi echo "${LOCAL_GROUP}" return fi # Otherwise create the group with requested GID, unless GID is taken. if getent group "${LOCAL_GID}" >/dev/null 2>&1; then # Some other group holds the target GID; return that group's name. getent group "${LOCAL_GID}" | cut -d: -f1 else groupadd -g "${LOCAL_GID}" "${LOCAL_GROUP}" echo "${LOCAL_GROUP}" fi } ensure_user() { # If a user with the desired UID already exists, use that user instead if getent passwd "${LOCAL_UID}" >/dev/null 2>&1; then existing_user=$(getent passwd "${LOCAL_UID}" | cut -d: -f1) echo "Host UID ${LOCAL_UID} already exists in container as user '${existing_user}'; will use that account." LOCAL_USER="${existing_user}" return fi # If the desired username already exists, try to change its UID (only if the target UID is free) if id -u "${LOCAL_USER}" >/dev/null 2>&1; then current_uid=$(id -u "${LOCAL_USER}") if [[ "${current_uid}" != "${LOCAL_UID}" ]]; then if ! getent passwd "${LOCAL_UID}" >/dev/null 2>&1; then usermod -u "${LOCAL_UID}" "${LOCAL_USER}" || echo "Warning: cannot change UID for ${LOCAL_USER}, leaving as ${current_uid}" >&2 else echo "Warning: target UID ${LOCAL_UID} already in use; leaving user ${LOCAL_USER} with UID ${current_uid}" >&2 fi fi else # Create the user only if the target UID is free if getent passwd "${LOCAL_UID}" >/dev/null 2>&1; then # Someone owns the UID, pick that user existing_user=$(getent passwd "${LOCAL_UID}" | cut -d: -f1) echo "Host UID ${LOCAL_UID} in use by '${existing_user}'; using that user inside container." LOCAL_USER="${existing_user}" else useradd -m -u "${LOCAL_UID}" -g "${LOCAL_GID}" -s /bin/bash "${LOCAL_USER}" || { echo "Warning: failed to create user ${LOCAL_USER} with UID ${LOCAL_UID}; falling back to root inside container." >&2 LOCAL_USER=root } fi fi # Ensure home dir exists and is owned by requested uid/gid where possible mkdir -p "/home/${LOCAL_USER}" chown -R "${LOCAL_UID}:${LOCAL_GID}" "/home/${LOCAL_USER}" 2>/dev/null || true } setup_sudo() { if [[ "${ENABLE_CONTAINER_SUDO}" == "1" ]]; then usermod -aG sudo "${LOCAL_USER}" || true echo "${LOCAL_USER} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/90-local-user chmod 0440 /etc/sudoers.d/90-local-user else rm -f /etc/sudoers.d/90-local-user fi } fix_mount_ownership() { mkdir -p "/home/${LOCAL_USER}/.ssh" chown -R "${LOCAL_UID}:${LOCAL_GID}" "/home/${LOCAL_USER}/.ssh" 2>/dev/null || true chown "${LOCAL_UID}:${LOCAL_GID}" "/home/${LOCAL_USER}/.gitconfig" 2>/dev/null || true chown "${LOCAL_UID}:${LOCAL_GID}" "/home/${LOCAL_USER}/.bashrc" 2>/dev/null || true chown "${LOCAL_UID}:${LOCAL_GID}" "/home/${LOCAL_USER}/.bash_aliases" 2>/dev/null || true } ensure_group >/dev/null ensure_user setup_sudo fix_mount_ownership export HOME="/home/${LOCAL_USER}" cd "${WORKDIR_IN_CONTAINER}" exec runuser -u "${LOCAL_USER}" -- "$@" DOCKER_SCRIPT