ubin/docker-wine

630 lines
21 KiB
Bash
Executable File

#!/usr/bin/env bash
print_help () {
echo "Usage: $0 [OPTION[=VALUE]]... [COMMAND [ARGS]...]..."
echo
echo "Run the docker-wine container with behaviour determined by the following"
echo "OPTIONS:"
echo " --cache Use the cached image pulled from Docker Hub and don't"
echo " attempt to pull the latest version"
echo " --local Use locally built docker-wine image instead of pulling"
echo " image from Docker Hub"
echo " --local=VALUE Specify an alternate locally built image and use instead"
echo " of pulling image from Docker Hub"
echo " --rm Start the container in non-persistent mode. i.e. Without"
echo " mounting the user's home to a volume or path on the"
echo " host"
echo " --tag=VALUE Specify an image tag to use (default is latest)"
echo " --name=VALUE Name of the docker container instantiated"
echo " (default is 'wine')"
echo " --arm64 Start the containter using linux/arm64 platform for ARM"
echo " and Apple Silicon (M1) native image"
echo " --as-root Start the container as root"
echo " --as-me Start the container using your current username, UID and"
echo " GID (default when alternate --home value specified)"
echo " --notty Start container attached with no tty"
echo " --nordp Use a version of the docker-wine image that does not"
echo " include RDP server packages for a reduced image size"
echo " --rdp Shortcut for --rdp=interactive"
echo " --rdp=OPTION Runs docker-wine container with Remote Desktop Protocol"
echo " server"
echo " Valid values for OPTION are:"
echo " no Don't use RDP server (default)"
echo " start Start the RDP server as a detached"
echo " daemon"
echo " stop Stop the detached RDP server by"
echo " stopping the container"
echo " restart Restart the detached RDP server by"
echo " stopping and starting the container"
echo " interactive Start the RDP server and also run an"
echo " interactive bash session"
echo " --rdp-port=VALUE Bind RDP to a different TCP port (default is 3389)"
echo " --shm-size=VALUE Set the shared memory size (default is '1g' for 1 GB)"
echo " --xvfb[=OPTIONAL] Start xvfb"
echo " OPTIONAL consists of comma separated values of:"
echo " SERVER_NR Server number to use, eg. :1"
echo " SCREEN Screen number to use, eg. 0"
echo " RESOLUTION Screen resolution, eg. 320x240x8"
echo " If OPTIONAL is left blank, defaults to:"
echo " :95,0,320x240x8"
echo " --sound=OPTION Select a pulseaudio configuration for sound output when"
echo " running in X11 forwarding mode"
echo " Valid values for OPTION are:"
echo " default Use the default pulseaudio config for"
echo " host OS (unix is default for Linux,"
echo " dummy is default for macOS)"
echo " unix Use UNIX socket '/tmp/pulse-socket' to"
echo " connect to the host machine's"
echo " pulseaudio server (Linux only)"
echo " dummy Run pulseaudio server in container with"
echo " dummy (null) output"
echo " none Alias for dummy"
echo " --home-volume=VALUE Use an alternate volume to winehome for storing"
echo " persistent user data. Valid values can specify either"
echo " a docker volume or local path"
echo " e.g."
echo " --home=my_new_volume"
echo " --home=/tmp/my_user"
echo " --home=VALUE Specify an alternate path for the user's home within the"
echo " container (default is /home/\$USER)"
echo " --force-owner Allow the user to take ownership of a home volume that"
echo " belongs to another user (NOTE: Use with caution!)"
echo " --nosudo Disable sudo for the user"
echo " --password=VALUE Specify a password for the user in plain text (default"
echo " is the user's username)"
echo " --password-prompt Prompt to set a password for the user"
echo " --secure-password=VALUE Provide an encrypted password for the user"
echo " --device=VALUE Bind device(s) to container. Uses standard docker"
echo " syntax and multiple statements are allowed"
echo " --env=VALUE Specify additional environment variable(s) to be passed"
echo " to the container. Uses standard docker syntax and"
echo " multiple statements are allowed"
echo " --mount=VALUE Specify additional directory bindings. Uses"
echo " standard docker syntax and multiple statements are"
echo " allowed"
echo " --network=VALUE Specify the network to connect the container to. Uses"
echo " standard docker syntax"
echo " --volume=VALUE Specify additional volume(s) to be mounted. Uses"
echo " standard docker syntax and multiple statements are"
echo " allowed"
echo " --workdir=VALUE Specify alternate WORKDIR (default is \$HOME)"
echo " --help Display this help screen and exit"
echo
echo "e.g."
echo " $0"
echo " $0 wine notepad"
echo " $0 wineboot --init"
echo " $0 --local --volume=my_vol:/some/path:ro"
echo " $0 --local --as-me wine notepad"
echo " $0 --as-root --rdp"
echo " $0 --rdp=start --password=pa55w0rd"
}
add_run_arg () {
RUN_ARGS+=("$1")
}
add_run_args_for_as_me () {
USER_HOME="${HOME}"
WORKDIR="${USER_HOME}"
add_run_arg --env="USER_NAME=$(whoami)"
add_run_arg --env="USER_UID=$(id -u)"
add_run_arg --env="USER_GID=$(id -g)"
add_run_arg --env="USER_HOME=${USER_HOME}"
}
encrypt_password () {
local password="$1"
local encrypted_password
if [ -z "${password}" ]; then
echo "ERROR: Password cannot be left blank"
exit 1
fi
encrypted_password="$(openssl passwd -1 -salt "$(openssl rand -base64 6)" "${password}")"
# Add encrypted password to run args
add_run_arg --env="USER_PASSWD=${encrypted_password}"
}
add_run_arg_timezone () {
local tz
if [ -f "/etc/timezone" ]; then
tz="$(cat /etc/timezone)"
elif [ -f "/etc/localtime" ]; then
tz="$(readlink /etc/localtime | awk -F/ '{print $(NF-1)"/"$NF}')"
else
tz="UTC"
fi
add_run_arg --env="TZ=${tz}"
}
configure_xquartz () {
# Return 0 (true) if this function makes any changes
local changes_made=1
# Check XQuartz installed
if ! [ -f /opt/X11/bin/xquartz ]; then
local answer
local attempts
local max_attempts=5
# Prompt to allow install
echo "XQuartz needs to be installed for X11 forwarding to operate. If necessary, Homebrew will also be installed to perform the installation of XQuartz."
for (( attempts = 0; attempts < max_attempts; attempts++ )); do
read -r -p "Do you want to continue? [y/N] " answer
# Default is No
[ -z "${answer}" ] && answer="n"
case "${answer}" in
[Yy]|[Yy][Ee][Ss])
install_xquartz || exit 1
changes_made=0
break
;;
[Nn]|[Nn][Oo])
echo "Unable to start container with X11 forwarding. Please install XQuartz or alternatively use Remote Desktop. e.g. $0 --rdp"
exit 0
;;
*)
echo "Invalid response. Please use y or n"
;;
esac
done
# Fail after too many attempts
if [ "${attempts}" -ge "${max_attempts}" ]; then
echo "ERROR: Too many invalid responses"
exit 1
fi
fi
# Configure XQuartz
if [ -e ~/Library/Preferences/org.xquartz.X11.plist ] ; then
xquartz_properties=org.xquartz.X11
else xquartz_properties=org.macosforge.xquartz.X11; fi
if [ "$(defaults read $xquartz_properties app_to_run)" != "/usr/bin/true" ]; then
defaults write $xquartz_properties app_to_run /usr/bin/true
changes_made=0
fi
if [ "$(defaults read $xquartz_properties nolisten_tcp)" != "0" ]; then
defaults write $xquartz_properties nolisten_tcp 0
changes_made=0
fi
if [ "$(defaults read $xquartz_properties enable_iglx)" != "1" ]; then
# Enable GLX (OpenGL)
defaults write $xquartz_properties enable_iglx -bool true
changes_made=0
fi
return $changes_made
}
install_xquartz() {
# Return 0 if XQuartz is successfully installed
local installed=1
# Install Homebrew
if ! command -v brew >/dev/null 2>&1; then
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
# Confirm installed
if ! command -v brew >/dev/null 2>&1; then
echo "ERROR: Failed to install Homebrew, unable to proceed with XQuartz installation"
exit 1
fi
fi
# Install XQuartz
if ! [ -f /opt/X11/bin/xquartz ]; then
brew install --cask xquartz
# Confirm installed
[ -f /opt/X11/bin/xquartz ] && installed=0
fi
return $installed
}
add_x11_key () {
local display="$1"
# Check for .Xauthority which is required for authenticating as the current user on the host's X11 server
if [ -z "${XAUTHORITY:-${HOME}/.Xauthority}" ]; then
echo "ERROR: No valid .Xauthority file found for X11"
exit 1
fi
# Get the hex key for the display from host user's .Xauthority file and store in ~/.docker-wine.Xkey
xauth list "${display}" | head -n1 | awk '{print $3}' > ~/.docker-wine.Xkey
# Lock down permissions
chmod 600 ~/.docker-wine.Xkey
# Add .Xkey to the run args
add_run_arg --volume="${HOME}/.docker-wine.Xkey:/root/.Xkey:ro"
}
configure_sound () {
local os="$1"
case "${os}" in
linux)
if [ "${SOUND}" == "default" ]; then
SOUND="unix"
fi
;;
macos)
if [ "${SOUND}" == "default" ]; then
SOUND="dummy"
fi
;;
*)
echo "ERROR: '${os}' is not a valid OS string for configuring sound"
exit 1
;;
esac
case "${SOUND}" in
unix)
configure_pulseaudio_unix_socket
;;
dummy|none)
add_run_arg --env="DUMMY_PULSEAUDIO=yes"
;;
*)
echo "ERROR: '${SOUND}' is not a valid option for configuring sound"
exit 1
;;
esac
}
configure_pulseaudio_unix_socket () {
# Use audio if pulseaudio is installed
if command -v pulseaudio >/dev/null 2>&1; then
# One-off setup for creation of UNIX socket for pulseaudio to allow access for other users
if [ ! -f "${HOME}/.config/pulse/default.pa" ]; then
echo "INFO: Creating pulseaudio config file ${HOME}/.config/pulse/default.pa"
mkdir -p "${HOME}/.config/pulse"
echo -e ".include /etc/pulse/default.pa\nload-module module-native-protocol-unix auth-anonymous=1 socket=/tmp/pulse-socket" > "${HOME}/.config/pulse/default.pa"
fi
# Restart pulseaudio daemon to create the UNIX socket
if [ ! -e "/tmp/pulse-socket" ]; then
echo "INFO: No socket found for pulseaudio so restarting service..."
pulseaudio -k
pulseaudio --start
sleep 1
fi
# Add the pulseaudio UNIX socket to run args
if [ -e "/tmp/pulse-socket" ]; then
add_run_arg --volume="/tmp/pulse-socket:/tmp/pulse-socket"
else
echo "INFO: pulseaudio socket /tmp/pulse-socket doesn't exist, so sound will not function"
fi
else
echo "INFO: pulseaudio not installed so running without sound"
fi
}
run_container () {
local mode
case "$1" in
interactive)
mode="-it"
;;
detached)
mode="--detach"
;;
*)
echo "ERROR: '$1' is not a valid container run mode"
exit 1
;;
esac
# Add common docker run args
add_run_arg --rm
add_run_arg --hostname="$(hostname)"
add_run_arg --name="${CONTAINER_NAME}"
add_run_arg --shm-size="${SHM_SIZE}"
add_run_arg --workdir="${WORKDIR}"
add_run_arg_timezone
# Append -nordp to image tag if NO_RDP is set to 'yes'
if [ "${NO_RDP}" == "yes" ]; then
# Only append if image tag specified does not already end in -nordp
if ! echo "${IMAGE_TAG}" | grep -q -E "\-nordp$"; then
IMAGE_TAG="${IMAGE_TAG}-nordp"
fi
fi
# Grab the latest image from docker hub or use the locally built version
if [ "${USE_LOCAL_IMAGE}" == "no" ]; then
[ "${DOCKER_PULL_IMAGE}" == "yes" ] && docker pull "${DOCKER_IMAGE}:${IMAGE_TAG}"
else
DOCKER_IMAGE="${LOCAL_IMAGE}"
fi
# Add volume for user home only if not using --rm option
if [ "${NO_USER_VOLUME}" == "no" ]; then
add_run_arg --volume="${USER_VOLUME}:${USER_HOME}"
# Create the docker volume to store user's home only if using default winehome
if [ "${USER_VOLUME}" == "winehome" ] && ! docker volume ls -qf "name=winehome" | grep -q "^winehome$"; then
echo "INFO: Creating Docker volume container 'winehome'..."
docker volume create winehome
fi
fi
# NOTTY rules them all
if [ "${NOTTY}" == "yes" ] ; then
docker run "${RUN_ARGS[@]}" "${DOCKER_IMAGE}:${IMAGE_TAG}" "${CMD_ARGS[@]}"
else
docker run "${mode}" "${RUN_ARGS[@]}" "${DOCKER_IMAGE}:${IMAGE_TAG}" "${CMD_ARGS[@]}"
fi
}
# Set default values
CONTAINER_NAME="wine"
DOCKER_IMAGE="scottyhardy/docker-wine"
DOCKER_PULL_IMAGE="yes"
HOST_RDP_PORT="3389"
IMAGE_TAG="latest"
LOCAL_IMAGE="docker-wine"
NO_RDP="no"
NO_USER_VOLUME="no"
NOTTY="no"
SHM_SIZE="1g"
SOUND="default"
USE_LOCAL_IMAGE="no"
USE_RDP_SERVER="no"
USER_HOME="/home/wineuser"
USER_VOLUME="winehome"
WORKDIR="${USER_HOME}"
USE_XVFB="no"
XVFB_RESOLUTION="320x240x8"
XVFB_SCREEN="0"
XVFB_SERVER=":95"
# Array to store all of the `docker run` arguments
RUN_ARGS=()
while [ $# -gt 0 ]; do
case "$1" in
--cache)
DOCKER_PULL_IMAGE="no"
;;
--local)
USE_LOCAL_IMAGE="yes"
;;
--local=*)
USE_LOCAL_IMAGE="yes"
LOCAL_IMAGE="${1#*=}"
;;
--rm)
NO_USER_VOLUME="yes"
;;
--tag=*)
IMAGE_TAG="${1#*=}"
;;
--name=*)
CONTAINER_NAME="${1#*=}"
;;
--arm64)
add_run_arg --platform="linux/arm64"
IMAGE_TAG="arm64"
;;
--as-root)
add_run_arg --env="RUN_AS_ROOT=yes"
WORKDIR="/"
;;
--as-me)
add_run_args_for_as_me
;;
--notty)
NOTTY="yes"
;;
--nordp)
NO_RDP="yes"
;;
--rdp)
USE_RDP_SERVER="interactive"
;;
--rdp=*)
USE_RDP_SERVER="${1#*=}"
;;
--rdp-port=*)
HOST_RDP_PORT="${1#*=}"
;;
--shm-size=*)
SHM_SIZE="${1#*=}"
;;
--xvfb)
USE_XVFB="yes"
;;
--xvfb=*)
USE_XVFB="yes"
IFS=, read -r XVFB_SERVER XVFB_SCREEN XVFB_RESOLUTION <<< "${1#*=}"
;;
--sound=*)
SOUND="${1#*=}"
;;
--home-volume=*)
USER_VOLUME="${1#*=}"
# Start container as self to prevent unintentionally changing ownership of a user's local filesystem by wineuser
add_run_args_for_as_me
;;
--home=*)
USER_HOME="${1#*=}"
add_run_arg --env="USER_HOME=${USER_HOME}"
;;
--force-owner)
add_run_arg --env="FORCED_OWNERSHIP=yes"
;;
--nosudo)
add_run_arg --env="USER_SUDO=no"
;;
--password=*)
encrypt_password "${1#*=}"
;;
--password-prompt)
read -r -s -p "Password: " PASSWD
echo
encrypt_password "${PASSWD}"
;;
--secure-password=*)
add_run_arg --env="USER_PASSWD=${1#*=}"
;;
--device=*|--env=*|--mount=*|--network=*|--volume=*)
add_run_arg "$1"
;;
--workdir=*)
WORKDIR="${1#*=}"
;;
--help)
print_help
exit 0
;;
-*)
echo "ERROR: '$1' is not a valid option"
echo
print_help
exit 1
;;
*)
break
;;
esac
shift
done
# Collect remaining command line args to pass to the container to run
CMD_ARGS=("$@")
# Sanity checks
if ! docker system info >/dev/null 2>&1; then
echo "ERROR: Docker is not running or not installed, unable to proceed"
exit 1
fi
if ! echo "${USE_RDP_SERVER}" | grep -q -E "^(no|start|stop|restart|interactive)$"; then
echo "ERROR: '${USE_RDP_SERVER}' is not a valid value for --rdp option"
exit 1
fi
if [ "${USE_RDP_SERVER}" != "no" ] && [ -n "${CMD_ARGS[0]}" ]; then
echo "ERROR: Commands cannot be passed to container when using --rdp option"
exit 1
fi
if [ "${USE_RDP_SERVER}" != "no" ] && [ "${NO_RDP}" != "no" ]; then
echo "ERROR: Cannot combine conflicting options --rdp and --nordp"
exit 1
fi
# Run xvfb and send everything into the void
if [ "${USE_XVFB}" == "yes" ] ; then
add_run_arg --env="USE_XVFB=yes"
add_run_arg --env="XVFB_SERVER=${XVFB_SERVER}"
add_run_arg --env="XVFB_SCREEN=${XVFB_SCREEN}"
add_run_arg --env="XVFB_RESOLUTION=${XVFB_RESOLUTION}"
add_run_arg --env="DISPLAY=${XVFB_SERVER}"
run_container "interactive"
# Run in RDP mode
elif [ "${USE_RDP_SERVER}" != "no" ]; then
add_run_arg --env="RDP_SERVER=yes"
add_run_arg --publish="${HOST_RDP_PORT}:3389/tcp"
case "${USE_RDP_SERVER}" in
interactive)
CMD_ARGS=("/bin/bash")
run_container "interactive"
;;
start)
run_container "detached"
;;
stop)
docker kill "${CONTAINER_NAME}"
;;
restart)
docker kill "${CONTAINER_NAME}"
run_container "detached"
;;
*)
echo "ERROR: '${USE_RDP_SERVER}' is not a valid value for --rdp option"
exit 1
;;
esac
# Run in X11 forwarding mode
else
# Set CMD_ARGS to /bin/bash if no commands specified
[ -z "${CMD_ARGS[0]}" ] && CMD_ARGS=("/bin/bash")
# Run in X11 forwarding mode on macOS
if [ "$(uname)" == "Darwin" ]; then
# Advise to reboot if need to configure XQuartz
if configure_xquartz; then
echo "INFO: XQuartz configuration updated. Please reboot to enable X11 forwarding to operate."
exit 0
fi
# Ensure XQuartz is running so ~/.Xauthority file is updated with new X11 key
if ! ps -e | awk '{print $4}' | grep -q "/opt/X11/bin/Xquartz"; then
open -a xquartz
fi
# Store the X11 key for the display in ~/.docker-wine.Xkey and add to run args
add_x11_key ":0"
# Configure sound output
configure_sound "macos"
# Add macOS run args
add_run_arg --env="DISPLAY=host.docker.internal:0"
run_container "interactive"
# Run in X11 forwarding mode on Linux
elif [ "$(uname)" == "Linux" ]; then
# Store the X11 key for the display in ~/.docker-wine.Xkey and add to run args
add_x11_key "$DISPLAY"
# Configure sound output
configure_sound "linux"
# Add Linux run args
add_run_arg --env="DISPLAY"
add_run_arg --volume="/tmp/.X11-unix:/tmp/.X11-unix:ro"
run_container "interactive"
else
echo "ERROR: '$(uname)' OS is not supported"
exit 1
fi
fi