diff --git a/docs/advanced/nixos.md b/docs/advanced/nixos.md index cebc00a6..183e5564 100644 --- a/docs/advanced/nixos.md +++ b/docs/advanced/nixos.md @@ -72,10 +72,6 @@ environment variables in camelCase: wineExperimentalWayland = false; # Networking mode for the container ("bridge" is default) networking = "bridge"; - # User ID for running the container (usually your own UID) - zwiftUid = "1000"; - # Group ID for running the container (usually your own GID) - zwiftGid = "1000"; # GPU/device flags override (Docker: "--gpus=all", Podman/CDI: "--device=nvidia.com/gpu=all") vgaDeviceFlag = "--device=nvidia.com/gpu=all"; # Enable debug output and verbose logging if true diff --git a/docs/advanced/podman-support.md b/docs/advanced/podman-support.md index 5273836f..fce2e61c 100644 --- a/docs/advanced/podman-support.md +++ b/docs/advanced/podman-support.md @@ -15,7 +15,3 @@ From Podman 4.3 this became automatic by providing the Container UID/GID and pod For example if the host uid/gid is 1001/1001 then we need to map the host resources from `/run/user/1001` to the container resource `/run/user/1000` and map the user and group id's the same. This had to be done manually on the host podman start using `--uidmap` and `--gidmap` (not covered here). - -{: .warning } -Using ZWIFT_UID/GID will only work if the user starting podman has access to the `/run/user/$ZWIFT_UID` resources and does -not work the same way as in Docker so is not supported. diff --git a/docs/configuration/options.md b/docs/configuration/options.md index cc0a4bfe..74ee072a 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -79,8 +79,6 @@ These environment variables can be used to alter the execution of the zwift bash | [`ZWIFT_NO_GAMEMODE`](#zwift_no_gamemode) | `0` | If set to `1`, don't run game mode | | [`WINE_EXPERIMENTAL_WAYLAND`](#wine_experimental_wayland) | `0` | If set to `1`, use native Wayland | | [`NETWORKING`](#networking) | `bridge` | Sets the type of container networking to use | -| [`ZWIFT_UID`](#zwift_uid) | `$(id -u)` | Sets the UID that Zwift will run as | -| [`ZWIFT_GID`](#zwift_gid) | `$(id -g)` | Sets the GID that Zwift will run as | | [`VGA_DEVICE_FLAG`](#vga_device_flag) | | Override container GPU/device flags | | [`PRIVILEGED_CONTAINER`](#privileged_container) | `0` | If set to `1`, run the container in privileged mode | @@ -657,52 +655,6 @@ Configure how the container connects to the Internet. --- -### `ZWIFT_UID` - -See also [`ZWIFT_GID`](#zwift_gid). - -Use this option to launch Zwift from a different user id. - -| Item | Description | -|:------------------|:-------------------------| -| Allowed values | number | -| Default value | `$(id -u)` | -| Commandline usage | `ZWIFT_UID="1001" zwift` | -| Config file usage | `ZWIFT_UID="1001"` | - -{: .warning } -> It is strongly discouraged to use a `ZWIFT_UID` that is different from your user uid. If you decide to do so anyway, know -> that: -> -> - It does not work with podman -> - It does not work with Wayland -> - Cats and dogs may start living together - ---- - -### `ZWIFT_GID` - -See also [`ZWIFT_UID`](#zwift_uid). - -Use this option to launch Zwift from a different group id. - -| Item | Description | -|:------------------|:-------------------------| -| Allowed values | number | -| Default value | `$(id -g)` | -| Commandline usage | `ZWIFT_GID="1001" zwift` | -| Config file usage | `ZWIFT_GID="1001"` | - -{: .warning } -> It is strongly discouraged to use a `ZWIFT_GID` that is different from your user gid. If you decide to do so anyway, know -> that: -> -> - It does not work with podman -> - It does not work with Wayland -> - Cats and dogs may start living together - ---- - ### `VGA_DEVICE_FLAG` See also [Prerequisites for NVIDIA graphics cards][nvidia-prerequisites-href]. diff --git a/flake.nix b/flake.nix index 5cec3394..2186e41f 100644 --- a/flake.nix +++ b/flake.nix @@ -30,8 +30,6 @@ zwiftNoGameMode, wineExperimentalWayland, networking, - zwiftUid, - zwiftGid, vgaDeviceFlag, debug, verbosity, @@ -77,8 +75,6 @@ wineExperimentalWayland != "" ) "export WINE_EXPERIMENTAL_WAYLAND=${wineExperimentalWayland}"} ${pkgs.lib.optionalString (networking != "") "export NETWORKING='${networking}'"} - ${pkgs.lib.optionalString (zwiftUid != "") "export ZWIFT_UID='${zwiftUid}'"} - ${pkgs.lib.optionalString (zwiftGid != "") "export ZWIFT_GID='${zwiftGid}'"} ${pkgs.lib.optionalString (debug != "") "export DEBUG=${debug}"} ${pkgs.lib.optionalString (verbosity != "") "export VERBOSITY='${verbosity}'"} ${pkgs.lib.optionalString (vgaDeviceFlag != "") "export VGA_DEVICE_FLAG='${vgaDeviceFlag}'"} @@ -201,14 +197,6 @@ type = str; default = ""; }; - zwiftUid = mkOption { - type = str; - default = ""; - }; - zwiftGid = mkOption { - type = str; - default = ""; - }; vgaDeviceFlag = mkOption { type = str; default = ""; @@ -250,8 +238,6 @@ zwiftScreenshotsDir zwiftOverrideResolution networking - zwiftUid - zwiftGid vgaDeviceFlag verbosity ; diff --git a/src/Dockerfile b/src/Dockerfile index b0ea886d..e02eee50 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -1,4 +1,6 @@ ARG DEBIAN_VERSION=trixie +ARG USER_UID=1000 +ARG USER_GID=1000 FROM rust:1.90-slim AS build-runfromprocess @@ -27,6 +29,8 @@ FROM debian:${DEBIAN_VERSION}-slim AS wine-base # make sure to add "=" to the start, comment out for latest # WINE_VERSION="=9.9~bookworm-1" ARG DEBIAN_VERSION +ARG USER_UID +ARG USER_GID ARG WINE_BRANCH="devel" ARG WINE_VERSION="=9.9~${DEBIAN_VERSION}-1" ARG WINETRICKS_VERSION=20240105 @@ -35,11 +39,9 @@ ARG WINETRICKS_VERSION=20240105 # - ca-certificates for wget and curl # - curl used in zwift authentication script # - gamemode for freedesktop screensaver inhibit -# - gosu for invoking scripts in entrypoint # - libegl1 and libgl1 for GL library # - libvulkan1 for vulkan loader library # - procps for pgrep -# - sudo for normal user installation # - wget for downloading winehq key # - winbind for ntml_auth required by zwift/wine # - xdg-utils seems to be a dependency of wayland @@ -49,12 +51,10 @@ RUN dpkg --add-architecture i386 \ ca-certificates \ curl \ gamemode \ - gosu \ libegl1 \ libgl1 \ libvulkan1 \ procps \ - sudo \ wget \ winbind \ xdg-utils \ @@ -74,18 +74,27 @@ RUN wget -qO /etc/apt/trusted.gpg.d/winehq.asc https://dl.winehq.org/wine-builds && wget -qO /usr/local/bin/winetricks https://raw.githubusercontent.com/Winetricks/winetricks/${WINETRICKS_VERSION}/src/winetricks \ && chmod +x /usr/local/bin/winetricks -# Create passwordless user and make nvidia libraries discoverable -RUN adduser --disabled-password --gecos '' user \ - && adduser user sudo \ - && echo '%SUDO ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers \ +# Add audio driver config file +COPY pulse-client.conf /etc/pulse/client.conf + +# Various configuration +# - Create user +# - Create runtime directory +# - Update audio driver config file +# - Make nvidia libraries discoverable +RUN addgroup --gid ${USER_GID} user \ + && adduser --uid ${USER_UID} --gid ${USER_GID} --disabled-password --comment '' user \ + && sed -i "s|/run/user/1000|/run/user/${USER_UID}|g" /etc/pulse/client.conf \ + && mkdir -p /run/user/${USER_UID} \ + && chown -R user:user /run/user/${USER_UID} \ && echo "/usr/local/nvidia/lib" >> /etc/ld.so.conf.d/nvidia.conf \ && echo "/usr/local/nvidia/lib64" >> /etc/ld.so.conf.d/nvidia.conf -# Required for non-glvnd setups +# Make nvidia libraries discoverable (Required for non-glvnd setups) ENV LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu:/usr/lib/i386-linux-gnu:/usr/local/nvidia/lib:/usr/local/nvidia/lib64 -# Configure audio driver -COPY pulse-client.conf /etc/pulse/client.conf +# Run as a regular user instead of root +USER user FROM wine-base diff --git a/src/build-image.sh b/src/build-image.sh index 03e764f9..2cac69c4 100755 --- a/src/build-image.sh +++ b/src/build-image.sh @@ -73,9 +73,7 @@ readonly XAUTHORITY="${XAUTHORITY:-}" # Initialize script constants SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" > /dev/null 2>&1 && pwd)" -ZWIFT_UID="${UID}" -ZWIFT_GID="$(id -g)" -readonly SCRIPT_DIR ZWIFT_UID ZWIFT_GID +readonly SCRIPT_DIR # Initialize CONTAINER_TOOL: Use podman if available msgbox info "Looking for container tool" @@ -110,6 +108,10 @@ msgbox info "Image will be called ${IMAGE}" ############################### ##### Basic configuration ##### +# Create array for build arguments +declare -a build_args +build_args=() + # Create array for container arguments declare -a container_args container_args=( @@ -123,14 +125,22 @@ container_args=( -e VERBOSITY="${VERBOSITY}" -e COLORED_OUTPUT="${COLORED_OUTPUT_SUPPORTED}" -e CONTAINER_TOOL="${CONTAINER_TOOL}" - -e ZWIFT_UID="${ZWIFT_UID}" - -e ZWIFT_GID="${ZWIFT_GID}" ) +# Initialize user ids +host_uid="${UID}" +host_gid="$(id -g)" if [[ ${CONTAINER_TOOL} == "podman" ]]; then # Podman maps the local user into the container as uid/gid 1000 (the container's user), # consistent with zwift.sh. Using the host uid/gid here causes a uid mismatch at runtime. - container_args+=(--userns "keep-id:uid=1000,gid=1000") + container_uid=1000 + container_args+=(--userns="keep-id:uid=1000,gid=1000") +else + container_uid="${host_uid}" + build_args+=( + --build-arg USER_UID="${host_uid}" + --build-arg USER_GID="${host_gid}" + ) fi # Configure window manager @@ -145,8 +155,8 @@ container_args+=( ) if [[ -n ${XAUTHORITY} ]]; then container_args+=( - -e XAUTHORITY="${XAUTHORITY}" - -v "${XAUTHORITY}:${XAUTHORITY}" + -e XAUTHORITY="${XAUTHORITY//${host_uid}/${container_uid}}" + -v "${XAUTHORITY}:${XAUTHORITY//${host_uid}/${container_uid}}" ) elif command_exists xhost && xhost +local: > /dev/null; then msgbox ok "Container X11 access provided through xhost" @@ -185,7 +195,7 @@ cleanup() { trap cleanup EXIT msgbox info "Building image ${IMAGE}" -if ${CONTAINER_TOOL} build --force-rm -t "${BUILD_NAME}" "${SCRIPT_DIR}"; then +if ${CONTAINER_TOOL} build --force-rm "${build_args[@]}" -t "${BUILD_NAME}" "${SCRIPT_DIR}"; then msgbox ok "Successfully built image ${IMAGE}" else msgbox error "Failed to build image" diff --git a/src/entrypoint.sh b/src/entrypoint.sh index f278b5ab..f1085f4c 100755 --- a/src/entrypoint.sh +++ b/src/entrypoint.sh @@ -22,14 +22,10 @@ else fi readonly VERBOSITY="${VERBOSITY:-1}" -readonly ZWIFT_UID="${ZWIFT_UID:-$(id -u user)}" -readonly ZWIFT_GID="${ZWIFT_GID:-$(id -g user)}" readonly WINE_EXPERIMENTAL_WAYLAND="${WINE_EXPERIMENTAL_WAYLAND:-0}" readonly CONTAINER_TOOL="${CONTAINER_TOOL:?}" -readonly WINE_USER_HOME="/home/user/.wine/drive_c/users/user" readonly ZWIFT_HOME="/home/user/.wine/drive_c/Program Files (x86)/Zwift" -readonly ZWIFT_DOCS="${WINE_USER_HOME}/AppData/Local/Zwift" msgbox() { local type="${1:?}" # Type: info, ok, warning, error, debug @@ -56,6 +52,10 @@ msgbox() { esac } +is_user_root() { + [[ ${EUID} -eq 0 ]] +} + is_empty_directory() { local directory="${1:?}" if [[ ! -d ${directory} ]]; then @@ -66,8 +66,20 @@ is_empty_directory() { ! contents="$(ls -A "${directory}" 2> /dev/null)" || [[ -z ${contents} ]] } -########################### -##### Configure Zwift ##### +######################################### +##### Launch update or start script ##### + +msgbox info "Starting or installing Zwift" + +actual_user="$(whoami)" +actual_uid="$(id -u "${actual_user}")" +actual_gid="$(id -g "${actual_user}")" +msgbox debug "Running as ${actual_user} (uid=${actual_uid}, gid=${actual_gid})" + +if is_user_root; then + msgbox error "Cannot run or install Zwift as root!" + exit 1 +fi if ! mkdir -p "${ZWIFT_HOME}" || ! cd "${ZWIFT_HOME}"; then msgbox error "Zwift home directory '${ZWIFT_HOME}' does not exist or is not accessible!" @@ -78,113 +90,17 @@ fi # NOTE: DISPLAY must be unset here before run_zwift to work # Registry entries are set in the container install or won't work. if [[ ${WINE_EXPERIMENTAL_WAYLAND} -eq 1 ]]; then + msgbox info "Using Wayland, unsetting DISPLAY environment variable" unset DISPLAY fi -############################################ -##### Clean install, update or launch? ##### - declare -a startup_cmd startup_cmd=(/bin/run_zwift.sh) -update_required=0 if is_empty_directory "${ZWIFT_HOME}"; then startup_cmd=(/bin/update_zwift.sh --install) - update_required=1 elif [[ ${1:-} == "--update" ]]; then startup_cmd=(/bin/update_zwift.sh) - update_required=1 -fi - -###################################### -##### Change ownership if needed ##### - -if [[ ${CONTAINER_TOOL} == "docker" ]]; then - # with docker the container is launched as root - # here we update ids and ownership so zwift can be launched as user instead - - user_uid="$(id -u user)" - user_gid="$(id -g user)" - - should_change_user_ids() { - # ids should be updated if ZWIFT_UID:ZWIFT_GID is different from from user uid:gid - # returns 0 if ids should be changed, 1 if not, so it can be used in an if - - local result=1 - - if [[ ! ${ZWIFT_UID} =~ ^[0-9]+$ ]]; then - msgbox warning "Ignoring ZWIFT_UID '${ZWIFT_UID}' because it is not a number" - elif [[ ${user_uid} -ne ${ZWIFT_UID} ]]; then - user_uid="${ZWIFT_UID}" - result=0 - fi - - if [[ ! ${ZWIFT_GID} =~ ^[0-9]+$ ]]; then - msgbox warning "Ignoring ZWIFT_GID '${ZWIFT_GID}' because it is not a number" - elif [[ ${user_gid} -ne ${ZWIFT_GID} ]]; then - user_gid="${ZWIFT_GID}" - result=0 - fi - - return "${result}" - } - - change_user_ids() { - usermod -ou "${user_uid}" user || return 1 - groupmod -og "${user_gid}" user || return 1 - mkdir -p "/run/user/${user_uid}" || return 1 - chown -R user:user "/run/user/${user_uid}" || return 1 - sed -i "s|/run/user/1000|/run/user/${user_uid}|g" /etc/pulse/client.conf || return 1 - } - - ownership_needs_update() { - # Quick check: if the top-level directory is already owned by user:user, assume everything is fine - # This avoids a costly recursive find on every normal startup - local target="${1:?}" - local result - [[ -d ${target} ]] && result="$(find "${target}" -maxdepth 0 \( ! -user user -o ! -group user \) -print 2> /dev/null)" && [[ -n ${result} ]] - } - - update_ownership() { - local target - if [[ ${update_required} -eq 1 ]]; then - target="/home/user" - else - target="${ZWIFT_DOCS}" - fi - - if ! ownership_needs_update "${target}"; then - msgbox ok "Ownership already correct, skipping" - return 0 - fi - - # Only chown files that actually need it, rather than blindly recursing everything - msgbox info "Updating ownership of files in ${target} (this may take a while on first run)..." - find "${target}" \( ! -user user -o ! -group user \) -exec chown user:user {} + || return 1 - } - - if should_change_user_ids; then - msgbox info "Changing user ids to ${user_uid}:${user_gid}" - if change_user_ids; then - msgbox ok "Changed user ids" - else - msgbox error "Failed to change user ids" - exit 1 - fi - fi - - msgbox info "Checking file ownership" - if update_ownership; then - msgbox ok "File ownership is correct" - else - msgbox error "Failed to update file ownership" - exit 1 - fi - - startup_cmd=(gosu user:user "${startup_cmd[@]}") fi -######################################### -##### Launch update or start script ##### - "${startup_cmd[@]}" diff --git a/src/run_zwift.sh b/src/run_zwift.sh index 2de77306..e45aa852 100755 --- a/src/run_zwift.sh +++ b/src/run_zwift.sh @@ -66,6 +66,16 @@ if [[ ! -d ${ZWIFT_HOME} ]] || ! cd "${ZWIFT_HOME}"; then exit 1 fi +if [[ ! -d ${ZWIFT_DOCS} ]] || [[ ! -O ${ZWIFT_DOCS} ]] || [[ ! -G ${ZWIFT_DOCS} ]]; then + # shellcheck disable=SC2016 # using a command as literal string on the next line + expected_owner='$(id -u $USER):$(id -g $USER)' + [[ ${CONTAINER_TOOL} == "podman" ]] && expected_owner="1000:1000" + msgbox error "Directory ${ZWIFT_DOCS} does not exist or is not accessible." + msgbox error "You can try to fix its permissions by running:" + msgbox error " ${CONTAINER_TOOL} run --rm --user root -it -v zwift-\$USER:/zwift-docs --entrypoint bash netbrain/zwift:latest -c \"chown -R ${expected_owner} /zwift-docs\"" + exit 1 +fi + if [[ -n ${ZWIFT_OVERRIDE_RESOLUTION} ]]; then if [[ -f ${ZWIFT_PREFS} ]]; then msgbox info "Setting zwift resolution to ${ZWIFT_OVERRIDE_RESOLUTION}." diff --git a/src/zwift.sh b/src/zwift.sh index 8a4ad6ab..efd2939b 100755 --- a/src/zwift.sh +++ b/src/zwift.sh @@ -175,11 +175,18 @@ readonly ZWIFT_FG="${ZWIFT_FG:-0}" readonly ZWIFT_NO_GAMEMODE="${ZWIFT_NO_GAMEMODE:-0}" readonly WINE_EXPERIMENTAL_WAYLAND="${WINE_EXPERIMENTAL_WAYLAND:-0}" readonly NETWORKING="${NETWORKING:-bridge}" -readonly ZWIFT_UID="${ZWIFT_UID:-${UID}}" -readonly ZWIFT_GID="${ZWIFT_GID:-$(id -g)}" readonly VGA_DEVICE_FLAG="${VGA_DEVICE_FLAG:-}" readonly PRIVILEGED_CONTAINER="${PRIVILEGED_CONTAINER:-0}" +# No longer supported configuration environment variables +readonly ZWIFT_UID="${ZWIFT_UID:-}" +readonly ZWIFT_GID="${ZWIFT_GID:-}" +if [[ -n ${ZWIFT_UID} ]] || [[ -n ${ZWIFT_GID} ]]; then + msgbox error "ZWIFT_UID and ZWIFT_GID options are no longer supported" + msgbox error " To start Zwift as a different user, use: sudo -i username zwift" + exit 1 +fi + # Initialize CONTAINER_TOOL: Use podman if available msgbox info "Looking for container tool" CONTAINER_TOOL="${CONTAINER_TOOL:-}" @@ -206,8 +213,8 @@ declare -a parameters_to_print parameters_to_print=( DEBUG VERBOSITY CONTAINER_TOOL IMAGE VERSION SCRIPT_VERSION DONT_CHECK DONT_PULL DONT_CLEAN DRYRUN INTERACTIVE CONTAINER_EXTRA_ARGS ZWIFT_USERNAME ZWIFT_PASSWORD ZWIFT_WORKOUT_DIR ZWIFT_ACTIVITY_DIR ZWIFT_LOG_DIR ZWIFT_SCREENSHOTS_DIR - ZWIFT_OVERRIDE_GRAPHICS ZWIFT_OVERRIDE_RESOLUTION ZWIFT_FG ZWIFT_NO_GAMEMODE WINE_EXPERIMENTAL_WAYLAND NETWORKING ZWIFT_UID - ZWIFT_GID VGA_DEVICE_FLAG PRIVILEGED_CONTAINER DBUS_SESSION_BUS_ADDRESS DISPLAY WAYLAND_DISPLAY XAUTHORITY XDG_RUNTIME_DIR + ZWIFT_OVERRIDE_GRAPHICS ZWIFT_OVERRIDE_RESOLUTION ZWIFT_FG ZWIFT_NO_GAMEMODE WINE_EXPERIMENTAL_WAYLAND NETWORKING + VGA_DEVICE_FLAG PRIVILEGED_CONTAINER DBUS_SESSION_BUS_ADDRESS DISPLAY WAYLAND_DISPLAY XAUTHORITY XDG_RUNTIME_DIR ) for parameter_to_print in "${parameters_to_print[@]}"; do parameter_print_value="$(declare -p "${parameter_to_print}")" @@ -316,6 +323,9 @@ if [[ ${DONT_CLEAN} -ne 1 ]] && [[ ${DONT_PULL} -ne 1 ]]; then fi fi +container_image="${IMAGE}" +container_image_version="${VERSION}" + ############################### ##### Basic configuration ##### @@ -332,41 +342,15 @@ fi # Create array for container environment variables declare -a container_env_vars -container_env_vars=() - -# Create array for container arguments -declare -a container_args -container_args=() - -# Create array for entrypoint arguments -declare -a entrypoint_args -entrypoint_args=() - -if [[ ${CONTAINER_TOOL} == "podman" ]]; then - # Podman has to use container id 1000 - # Local user is mapped to the container id - local_uid="${ZWIFT_UID}" - container_uid=1000 - container_gid=1000 - container_args+=(--userns "keep-id:uid=${container_uid},gid=${container_gid}") -else - # Docker will run as the id's provided. - local_uid="${UID}" - container_uid="${ZWIFT_UID}" - container_gid="${ZWIFT_GID}" -fi - -# Define base container environment variables -container_env_vars+=( +container_env_vars=( DEBUG="${DEBUG}" VERBOSITY="${VERBOSITY}" - ZWIFT_UID="${container_uid}" - ZWIFT_GID="${container_gid}" CONTAINER_TOOL="${CONTAINER_TOOL}" ) -# Define base container parameters -container_args+=( +# Create array for container arguments +declare -a container_args +container_args=( --rm --network "${NETWORKING}" --name "zwift-${USER}" @@ -375,6 +359,10 @@ container_args+=( -v "zwift-${USER}:${ZWIFT_DOCS}" ) +# Create array for entrypoint arguments +declare -a entrypoint_args +entrypoint_args=() + ################################################### ##### Forward arguments passed to this script ##### @@ -392,6 +380,158 @@ for arg; do fi done +############################################# +##### Remap container user to host user ##### + +image_repo_digest() { + # Local images do not have a remote repository, will return non-zero + + local tag_name="${1:?}" + + ${CONTAINER_TOOL} inspect "${tag_name}" --format '{{index .RepoDigests 0}}' 2> /dev/null +} + +image_digest_label() { + local tag_name="${1:?}" + + ${CONTAINER_TOOL} inspect "${tag_name}" --format '{{index .Config.Labels "org.opencontainers.image.base.digest"}}' 2> /dev/null +} + +remap_build_required() { + local tag_name="${1:?}" + + local latest_image_digest + if latest_image_digest="$(image_repo_digest "${IMAGE}:${VERSION}")"; then + msgbox debug "Latest image digest is ${latest_image_digest}" + else + msgbox info "Failed to get ${IMAGE}:${VERSION} image repository digest, assuming rebuild is required" + return 0 + fi + + local current_image_digest + if current_image_digest="$(image_digest_label "${tag_name}")"; then + msgbox debug "Base image digest is ${current_image_digest}" + else + msgbox info "Failed to get ${tag_name} base image digest, may not exist yet, assuming rebuild is required" + return 0 + fi + + [[ ${current_image_digest} != "${latest_image_digest}" ]] +} + +create_remap_dockerfile() { + local user_uid="${1:?}" + local user_gid="${2:?}" + + local image_digest + if ! image_digest="$(image_repo_digest "${IMAGE}:${VERSION}")"; then + image_digest="Unknown" + fi + + echo "FROM ${IMAGE}:${VERSION}" + echo "USER root" + echo "RUN sed -i \"s|/run/user/\$(id -u user)|/run/user/${user_uid}|g\" /etc/pulse/client.conf \\" + echo " && usermod -ou ${user_uid} user \\" + echo " && groupmod -og ${user_gid} user \\" + echo " && mkdir -p /run/user/${user_uid} \\" + echo " && chown -R user:user /run/user/${user_uid}" + echo "USER user" + echo 'ENTRYPOINT ["entrypoint"]' + echo "LABEL org.opencontainers.image.base.digest=\"${image_digest}\"" +} + +build_remap_dockerfile() { + local tag_name="${1:?}" + local dockerfile="${2:?}" + + msgbox info "Building container image with remapped user" + + msgbox debug "Using dockerfile:" + local dockerfile_lines line + readarray -t dockerfile_lines <<< "${dockerfile}" + for line in "${dockerfile_lines[@]}"; do + msgbox debug " ${line/\\/\\\\}" + done + + if ${CONTAINER_TOOL} build -t "${tag_name}" - <<< "${dockerfile}"; then + msgbox info "Created ${CONTAINER_TOOL} image ${tag_name}" + else + msgbox error "Failed to create ${CONTAINER_TOOL} image ${tag_name}" + return 1 + fi +} + +host_uid="${UID}" +host_gid="$(id -g)" + +if [[ ${CONTAINER_TOOL} == "podman" ]]; then + container_uid=1000 + container_gid=1000 + + container_args+=(--userns="keep-id:uid=${container_uid},gid=${container_gid}") +else + container_uid="${host_uid}" + container_gid="${host_gid}" + + container_image="${IMAGE#docker.io/}" + container_image_version="uid_${container_uid}_gid_${container_gid}" + + msgbox info "Remapping container user to host user" + if remap_build_required "${container_image}:${container_image_version}"; then + remap_dockerfile="$(create_remap_dockerfile "${container_uid}" "${container_gid}")" + if build_remap_dockerfile "${container_image}:${container_image_version}" "${remap_dockerfile}"; then + msgbox ok "Remapped container user to host user" + else + msgbox error "Failed to remap container user to host user" + exit 1 + fi + else + msgbox ok "${container_image}:${container_image_version} is up to date" + fi +fi + +# Create the volume for the zwift documents directory if it does not already exist +if ! ${CONTAINER_TOOL} volume inspect "zwift-${USER}" > /dev/null 2>&1; then + msgbox info "Creating ${CONTAINER_TOOL} volume zwift-${USER}" + if ${CONTAINER_TOOL} volume create "zwift-${USER}" > /dev/null 2>&1; then + msgbox ok "Created volume zwift-${USER}" + else + msgbox error "Failed to create volume zwift-${USER}" + exit 1 + fi +fi + +volume_remap_required() { + ${CONTAINER_TOOL} run --rm \ + -v "zwift-${USER}:/zwift-docs" \ + --entrypoint bash \ + "${container_image}:${container_image_version}" \ + -c "[[ ! -O /zwift-docs ]] || [[ ! -G /zwift-docs ]]" +} + +remap_volume() { + ${CONTAINER_TOOL} run --rm \ + --user root \ + -v "zwift-${USER}:/zwift-docs" \ + --entrypoint bash \ + "${container_image}:${container_image_version}" \ + -c "chown -R \"${container_uid}:${container_gid}\" /zwift-docs" +} + +# Docker: Remap volume to container user +# Necessary in two cases: +# - Volume was just created, owner will be root, remap required +# - End user changed user uid/gid, remap required +if [[ ${CONTAINER_TOOL} != "podman" ]] && volume_remap_required; then + msgbox info "Updating owner of volume zwift-${USER}" + if remap_volume; then + msgbox ok "Updated zwift-${USER} volume owner to ${container_uid}:${container_gid}" + else + msgbox error "Failed to update zwift-${USER} volume owner to ${container_uid}:${container_gid}" + exit 1 + fi +fi + ############################################## ##### User defined environment variables ##### @@ -587,18 +727,13 @@ fi if [[ ${window_manager} == "Wayland" ]]; then msgbox info "Using Wayland window manager" - if [[ ${ZWIFT_UID} -ne ${UID} ]]; then - msgbox error "Wayland does not support ZWIFT_UID different to your id of ${UID}" - exit 1 - fi - if [[ -n ${XDG_RUNTIME_DIR} ]] && [[ -n ${WAYLAND_DISPLAY} ]]; then container_env_vars+=( - XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR//${local_uid}/${container_uid}}" + XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR//${host_uid}/${container_uid}}" WAYLAND_DISPLAY="${WAYLAND_DISPLAY}" WINE_EXPERIMENTAL_WAYLAND="1" ) - container_args+=(-v "${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}:${XDG_RUNTIME_DIR//${local_uid}/${container_uid}}/${WAYLAND_DISPLAY}") + container_args+=(-v "${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}:${XDG_RUNTIME_DIR//${host_uid}/${container_uid}}/${WAYLAND_DISPLAY}") else msgbox error "Required environment variables XDG_RUNTIME_DIR and/or WAYLAND_DISPLAY are not set" msgbox error "Falling back to XWayland" 5 @@ -625,8 +760,8 @@ if [[ ${window_manager} == "XWayland" ]] || [[ ${window_manager} == "XOrg" ]]; t fi if [[ -n ${XAUTHORITY} ]]; then - container_env_vars+=(XAUTHORITY="${XAUTHORITY//${local_uid}/${container_uid}}") - container_args+=(-v "${XAUTHORITY}:${XAUTHORITY//${local_uid}/${container_uid}}") + container_env_vars+=(XAUTHORITY="${XAUTHORITY//${host_uid}/${container_uid}}") + container_args+=(-v "${XAUTHORITY}:${XAUTHORITY//${host_uid}/${container_uid}}") else msgbox info "XAUTHORITY environment variable not set, container access to X11 needs to be granted with xhost" xhost_access_required=1 @@ -641,17 +776,17 @@ if [[ -n ${DBUS_SESSION_BUS_ADDRESS} ]]; then [[ ${DBUS_SESSION_BUS_ADDRESS} =~ ^unix:path=([^,]+) ]] dbus_unix_socket=${BASH_REMATCH[1]} if [[ -n ${dbus_unix_socket} ]]; then - container_env_vars+=(DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS//${local_uid}/${container_uid}}") - container_args+=(-v "${dbus_unix_socket}:${dbus_unix_socket//${local_uid}/${container_uid}}") + container_env_vars+=(DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS//${host_uid}/${container_uid}}") + container_args+=(-v "${dbus_unix_socket}:${dbus_unix_socket//${host_uid}/${container_uid}}") fi fi # Configure sound driver container_env_vars+=(PULSE_SERVER="/run/user/${container_uid}/pulse/native") -if [[ -d "/run/user/${local_uid}/pulse" ]]; then - container_args+=(-v "/run/user/${local_uid}/pulse:/run/user/${container_uid}/pulse") +if [[ -d "/run/user/${host_uid}/pulse" ]]; then + container_args+=(-v "/run/user/${host_uid}/pulse:/run/user/${container_uid}/pulse") else - msgbox warning "PulseAudio socket /run/user/${local_uid}/pulse not found — audio may not work (PipeWire-only system?)" + msgbox warning "PulseAudio socket /run/user/${host_uid}/pulse not found — audio may not work (PipeWire-only system?)" fi # Check for proprietary nvidia driver and set correct device to use (respects existing VGA_DEVICE_FLAG) @@ -674,7 +809,7 @@ fi ##### Start container ##### declare -a container_command -container_command=("${CONTAINER_TOOL}" run "${container_args[@]}" "${IMAGE}:${VERSION}" "${entrypoint_args[@]}") +container_command=("${CONTAINER_TOOL}" run "${container_args[@]}" "${container_image}:${container_image_version}" "${entrypoint_args[@]}") # Print the exact command that would be executed @@ -701,18 +836,6 @@ else print_container_command debug fi -# Create a volume if not already exists, this is done now as -# if left to the run command the directory can get the wrong permissions -if [[ ${CONTAINER_TOOL} == "podman" ]] && ! ${CONTAINER_TOOL} volume inspect "zwift-${USER}" > /dev/null 2>&1; then - msgbox info "Creating ${CONTAINER_TOOL} volume zwift-${USER}" - if ${CONTAINER_TOOL} volume create "zwift-${USER}"; then - msgbox ok "Created volume zwift-${USER}" - else - msgbox error "Failed to create volume zwift-${USER}" - exit 1 - fi -fi - # Only write environment variables to file when needed msgbox info "Writing environment variables to temporary file" if printf '%s\n' "${container_env_vars[@]}" > "${container_env_file}"; then