#!/bin/sh # SPDX-License-Identifier: GPL-3.0-only # # This file is part of the distrobox project: # https://github.com/89luca89/distrobox # # Copyright (C) 2021 distrobox contributors # # distrobox is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3 # as published by the Free Software Foundation. # # distrobox 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 distrobox; if not, see . # POSIX # Expected env variables: # HOME # USER # Optional env variables: # DBX_CONTAINER_ALWAYS_PULL # DBX_CONTAINER_CUSTOM_HOME # DBX_CONTAINER_GENERATE_ENTRY # DBX_CONTAINER_HOME_PREFIX # DBX_CONTAINER_HOSTNAME # DBX_CONTAINER_IMAGE # DBX_CONTAINER_MANAGER # DBX_CONTAINER_NAME # DBX_CONTAINER_CLEAN_PATH # DBX_NON_INTERACTIVE # DBX_VERBOSE # DBX_SKIP_WORKDIR # DBX_SUDO_PROGRAM # Ensure we have our env variables correctly set [ -z "${USER}" ] && USER="$(id -run)" [ -z "${HOME}" ] && HOME="$(getent passwd "${USER}" | cut -d':' -f6)" [ -z "${SHELL}" ] && SHELL="$(getent passwd "${USER}" | cut -d':' -f7)" app_cache_dir=${XDG_CACHE_HOME:-"${HOME}/.cache"}/distrobox trap cleanup TERM INT HUP EXIT # cleanup will remove fifo and temp files, and print to stdout # container's logs in case of error and verbose. # Arguments: # None # Expected global variables: # container_manager: string container manager to use # container_name: string container name # app_cache_dir: string cache dire to write file into # logs_pid: string pid of the podman/docker logs process # verbose: bool verbose # Expected env variables: # None # Outputs: # None cleanup() { rm -f "${app_cache_dir}/.${container_name}.fifo" if [ -n "${logs_pid:-}" ]; then kill "${logs_pid:-}" 2> /dev/null || : fi if [ "${verbose}" -eq 1 ]; then ${container_manager} logs "${container_name}" fi } # Despite of running this script via SUDO/DOAS being not supported (the # script itself will call the appropriate tool when necessary), we still want # to allow people to run it as root, logged in in a shell, and create rootful # containers. # # SUDO_USER is a variable set by SUDO and can be used to check whether the script was called by it. Same thing for DOAS_USER, set by DOAS. if { [ -n "${SUDO_USER}" ] || [ -n "${DOAS_USER}" ] } && [ "$(id -ru)" -eq 0 ]; then printf >&2 "Running %s via SUDO/DOAS is not supported. Instead, please try running:\n" "$(basename "${0}")" printf >&2 " %s --root %s\n" "$(basename "${0}")" "$*" exit 1 fi # Defaults # by default we use getent to get the login shell of the user and use that container_custom_command=0 container_command_user="$(echo "${USER}" | sed 's|\\|\\\\|g')" container_image_default="registry.fedoraproject.org/fedora-toolbox:latest" container_manager="autodetect" container_manager_additional_flags="" container_name="" container_name_default="my-distrobox" non_interactive=0 # Use cd + dirname + pwd so that we do not have relative paths in mount points # We're not using "realpath" here so that symlinks are not resolved this way # "realpath" would break situations like Nix or similar symlink based package # management. distrobox_enter_path="$(cd "$(dirname "$0")" && pwd)/distrobox-enter" dryrun=0 headless=0 # If the user runs this script as root in a login shell, set rootful=1. # There's no need for them to pass the --root flag option in such cases. [ "$(id -ru)" -eq 0 ] && rootful=1 || rootful=0 skip_workdir=0 verbose=0 clean_path=0 version="1.8.2.2" # Source configuration files, this is done in an hierarchy so local files have # priority over system defaults # leave priority to environment variables. # # On NixOS, for the distrobox derivation to pick up a static config file shipped # by the package maintainer the path must be relative to the script itself. self_dir="$(dirname "$(realpath "$0")")" nix_config_file="${self_dir}/../share/distrobox/distrobox.conf" config_files=" ${nix_config_file} /usr/share/distrobox/distrobox.conf /usr/share/defaults/distrobox/distrobox.conf /usr/etc/distrobox/distrobox.conf /usr/local/share/distrobox/distrobox.conf /etc/distrobox/distrobox.conf ${XDG_CONFIG_HOME:-"${HOME}/.config"}/distrobox/distrobox.conf ${HOME}/.distroboxrc " for config_file in ${config_files}; do # Shellcheck will give error for sourcing a variable file as it cannot follow # it. We don't care so let's disable this linting for now. # shellcheck disable=SC1090 [ -e "${config_file}" ] && . "$(realpath "${config_file}")" done # If we're running this script as root -- as in, logged in in the shell as root # user, and not via SUDO/DOAS --, we don't need to set distrobox_sudo_program # as it's meaningless for this use case. if [ "$(id -ru)" -ne 0 ]; then # If the DBX_SUDO_PROGRAM/distrobox_sudo_program variable was set by the # user, use its value instead of "sudo". But only if not running the script # as root (UID 0). distrobox_sudo_program=${DBX_SUDO_PROGRAM:-${distrobox_sudo_program:-"sudo"}} fi [ -n "${DBX_CONTAINER_MANAGER}" ] && container_manager="${DBX_CONTAINER_MANAGER}" [ -n "${DBX_CONTAINER_NAME}" ] && container_name="${DBX_CONTAINER_NAME}" [ -n "${DBX_CONTAINER_CLEAN_PATH}" ] && clean_path=1 [ -n "${DBX_SKIP_WORKDIR}" ] && skip_workdir="${DBX_SKIP_WORKDIR}" [ -n "${DBX_NON_INTERACTIVE}" ] && non_interactive="${DBX_NON_INTERACTIVE}" [ -n "${DBX_VERBOSE}" ] && verbose="${DBX_VERBOSE}" # Fixup variable=[true|false], in case we find it in the config file(s) [ "${non_interactive}" = "true" ] && non_interactive=1 [ "${non_interactive}" = "false" ] && non_interactive=0 [ "${verbose}" = "true" ] && verbose=1 [ "${verbose}" = "false" ] && verbose=0 # show_help will print usage to stdout. # Arguments: # None # Expected global variables: # version: distrobox version # Expected env variables: # None # Outputs: # print usage with examples. show_help() { cat << EOF distrobox version: ${version} Usage: distrobox-enter --name fedora-39 -- bash -l distrobox-enter my-alpine-container -- sh -l distrobox-enter --additional-flags "--preserve-fds" --name test -- bash -l distrobox-enter --additional-flags "--env MY_VAR=value" --name test -- bash -l MY_VAR=value distrobox-enter --additional-flags "--preserve-fds" --name test -- bash -l Options: --name/-n: name for the distrobox default: my-distrobox --/-e: end arguments execute the rest as command to execute at login default: default ${USER}'s shell --clean-path: reset PATH inside container to FHS standard --no-tty/-T: do not instantiate a tty --no-workdir/-nw: always start the container from container's home directory --additional-flags/-a: additional flags to pass to the container manager command --help/-h: show this message --root/-r: launch podman/docker/lilipod with root privileges. Note that if you need root this is the preferred way over "sudo distrobox" (note: if using a program other than 'sudo' for root privileges is necessary, specify it through the DBX_SUDO_PROGRAM env variable, or 'distrobox_sudo_program' config variable) --dry-run/-d: only print the container manager command generated --verbose/-v: show more verbosity --version/-V: show version EOF } # Parse arguments while :; do case $1 in -h | --help) # Call a "show_help" function to display a synopsis, then exit. show_help exit 0 ;; -v | --verbose) shift verbose=1 ;; -T | -H | --no-tty) shift headless=1 ;; -r | --root) shift rootful=1 ;; -V | --version) printf "distrobox: %s\n" "${version}" exit 0 ;; -d | --dry-run) shift dryrun=1 ;; -nw | --no-workdir) shift skip_workdir=1 ;; -n | --name) if [ -n "$2" ]; then container_name="$2" shift shift fi ;; -a | --additional-flags) if [ -n "$2" ]; then if [ -z "${container_manager_additional_flags=}" ]; then container_manager_additional_flags="$(echo "${2}" | sed -E "s/(--[a-zA-Z]+) ([^ ]+)/\1=\2/g" | sed 's/ --/\n--/g')" else container_manager_additional_flags="${container_manager_additional_flags} $(echo "${2}" | sed -E "s/(--[a-zA-Z]+) ([^ ]+)/\1=\2/g" | sed 's/ --/\n--/g')" fi shift shift fi ;; -Y | --yes) non_interactive=1 shift ;; -e | --exec | --) container_custom_command=1 shift # We pass the rest of arguments as $@ at the end break ;; --clean-path) shift clean_path=1 ;; -*) # Invalid options. printf >&2 "ERROR: Invalid flag '%s'\n\n" "$1" show_help exit 1 ;; *) # Default case: If no more options then break out of the loop. # If we have a flagless option and container_name is not specified # then let's accept argument as container_name if [ -n "$1" ]; then container_name="$1" shift else break fi ;; esac done set -o errexit set -o nounset # set verbosity if [ "${verbose}" -ne 0 ]; then set -o xtrace fi if [ -z "${container_name}" ]; then container_name="${container_name_default}" fi if [ ! -t 0 ] || [ ! -t 1 ]; then headless=1 fi # We depend on a container manager let's be sure we have it # First we use podman, else docker, else lilipod case "${container_manager}" in autodetect) if command -v podman > /dev/null; then container_manager="podman" elif command -v podman-launcher > /dev/null; then container_manager="podman-launcher" elif command -v docker > /dev/null; then container_manager="docker" elif command -v lilipod > /dev/null; then container_manager="lilipod" fi ;; podman) container_manager="podman" ;; podman-launcher) container_manager="podman-launcher" ;; lilipod) container_manager="lilipod" ;; docker) container_manager="docker" ;; *) printf >&2 "Invalid input %s.\n" "${container_manager}" printf >&2 "The available choices are: 'autodetect', 'podman', 'docker', 'lilipod'\n" ;; esac # Be sure we have a container manager to work with. if ! command -v "${container_manager}" > /dev/null && [ "${dryrun}" -eq 0 ]; then # Error: we need at least one between docker, podman or lilipod. printf >&2 "Missing dependency: we need a container manager.\n" printf >&2 "Please install one of podman, docker or lilipod.\n" printf >&2 "You can follow the documentation on:\n" printf >&2 "\tman distrobox-compatibility\n" printf >&2 "or:\n" printf >&2 "\thttps://github.com/89luca89/distrobox/blob/main/docs/compatibility.md\n" exit 127 fi # add verbose if -v is specified if [ "${verbose}" -ne 0 ]; then container_manager="${container_manager} --log-level debug" fi # prepend sudo (or the specified sudo program) if we want our container manager to be rootful if [ "${rootful}" -ne 0 ]; then container_manager="${distrobox_sudo_program-} ${container_manager}" fi # generate_enter_command will produce a Podman, Docker or Lilipod command to execute to enter the container. # Arguments: # None # Expected global variables: # container_manager: string container manager to use # container_name: string container name # container_manager_additional_flags: string container manager additional flags to use # container_home: string container's home path # container_path: string container's default PATH variable # headless: bool headless mode # skip_workdir: bool skip workdir # verbose: bool verbose # unshare_groups # distrobox_enter_path # Expected env variables: # PATH # USER # PWD # XDG_DATA_DIRS # XDG_CONFIG_DIRS # Outputs: # prints the podman, docker or lilipod command to enter the distrobox container generate_enter_command() { result_command="exec" result_command="${result_command} --interactive" result_command="${result_command} --detach-keys=" # In case of initful systems or unshared groups, we don't enter directly # as our user, but we instead enter as root, and then su $USER, in order # to trigger a proper login if [ "${unshare_groups:-0}" -eq 1 ]; then result_command="${result_command} --user=root" else result_command="${result_command} --user=${USER}" fi # For some usage, like use in service, or launched by non-terminal # eg. from desktop files, TTY can fail to instantiate, and fail to enter # the container. # To work around this, --headless let's you skip the --tty flag and make it # work in tty-less situations. # Disable tty also if we're NOT in a tty (test -t 0, test -t 1). if [ "${headless}" -eq 0 ]; then result_command="${result_command} --tty" fi # Entering container using our user and workdir. # Start container from working directory. Else default to home. Else do /. # Since we are entering from host, drop at workdir through '/run/host' # which represents host's root inside container. Any directory on host # even if not explicitly mounted is bound to exist under /run/host. # Since user $HOME is very likely present in container, enter there directly # to avoid confusing the user about shifted paths. # pass distrobox-enter path, it will be used in the distrobox-export tool. if [ "${skip_workdir}" -eq 0 ]; then workdir="${PWD:-${container_home:-"/"}}" if [ -n "${workdir##*"${container_home}"*}" ]; then workdir="/run/host${workdir}" fi else # Skipping workdir we just enter $HOME of the container. workdir="${container_home}" fi result_command="${result_command} --workdir=${workdir}" result_command="${result_command} --env=CONTAINER_ID=${container_name}" result_command="${result_command} --env=DISTROBOX_ENTER_PATH=${distrobox_enter_path}" # Loop through all the environment vars # and export them to the container. set +o xtrace # disable logging for this snippet, or it will be too talkative. # We filter the environment so that we do not have strange variables or # multiline. # We also NEED to ignore the HOME variable, as this is set at create time # and needs to stay that way to use custom home dirs. or it will be too talkative. result_command="${result_command} $(printenv | grep '=' | grep -Ev '"|`|\$' | grep -Ev '^(CONTAINER_ID|FPATH|HOST|HOSTNAME|HOME|PATH|PROFILEREAD|SHELL|XDG_SEAT|XDG_VTNR|XDG_.*_DIRS|^_)' | sed 's/ /\ /g' | sed 's/^\(.*\)$/--env=\1/g')" # Start with the $PATH set in the container's config container_paths="${container_path:-""}" # Ensure the standard FHS program paths are in PATH environment standard_paths="/usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /sbin /bin" if [ "${clean_path}" -eq 1 ]; then # only add the standard paths for standard_path in ${standard_paths}; do if [ -z "${container_paths}" ]; then container_paths="${standard_path}" else container_paths="${container_paths}:${standard_path}" fi done else # collect standard paths not existing from host PATH for standard_path in ${standard_paths}; do pattern="(:|^)${standard_path}(:|$)" if ! echo "${PATH}" | grep -Eq "${pattern}"; then if [ -z "${container_paths}" ]; then container_paths="${standard_path}" else container_paths="${container_paths}:${standard_path}" fi fi done # append additional standard paths to host PATH to get final container_paths if [ -n "${container_paths}" ]; then container_paths="${PATH}:${container_paths}" else container_paths="${PATH}" fi fi result_command="${result_command} --env=PATH=${container_paths}" # Ensure the standard FHS program paths are in XDG_DATA_DIRS environment standard_paths="/usr/local/share /usr/share" container_paths="${XDG_DATA_DIRS:-}" # add to the XDG_DATA_DIRS only after the host's paths, and only if not already present. for standard_path in ${standard_paths}; do pattern="(:|^)${standard_path}(:|$)" if [ -z "${container_paths}" ]; then container_paths="${standard_path}" elif ! echo "${container_paths}" | grep -Eq "${pattern}"; then container_paths="${container_paths}:${standard_path}" fi done result_command="${result_command} --env=XDG_DATA_DIRS=${container_paths}" # This correctly sets the XDG_* dirs to the container_home # it will be $HOME if using regular home dirs # if will be $container_home if using a custom home during create result_command="${result_command} --env=XDG_CACHE_HOME=${container_home}/.cache --env=XDG_CONFIG_HOME=${container_home}/.config --env=XDG_DATA_HOME=${container_home}/.local/share --env=XDG_STATE_HOME=${container_home}/.local/state" # Ensure the standard FHS program paths are in XDG_CONFIG_DIRS environment standard_paths="/etc/xdg" container_paths="${XDG_CONFIG_DIRS:-}" # add to the XDG_CONFIG_DIRS only after the host's paths, and only if not already present. for standard_path in ${standard_paths}; do pattern="(:|^)${standard_path}(:|$)" if [ -z "${container_paths}" ]; then container_paths="${standard_path}" elif ! echo "${container_paths}" | grep -Eq "${pattern}"; then container_paths="${container_paths}:${standard_path}" fi done result_command="${result_command} --env=XDG_CONFIG_DIRS=${container_paths}" # re-enable logging if it was enabled previously. if [ "${verbose}" -ne 0 ]; then set -o xtrace fi # Add additional flags if [ -n "${container_manager_additional_flags}" ]; then result_command="${result_command} ${container_manager_additional_flags}" fi # Run selected container with specified command. result_command="${result_command} ${container_name}" # Return generated command. # here we remove tabs as an artifact of using indentation in code to improve # readability printf "%s\n" "${result_command}" | tr -d '\t' } container_home="${HOME}" container_path="${PATH}" unshare_groups=0 # Now inspect the container we're working with. container_status="unknown" eval "$(${container_manager} inspect --type container --format \ 'container_status={{.State.Status}}; unshare_groups={{ index .Config.Labels "distrobox.unshare_groups" }}; {{range .Config.Env}}{{if and (ge (len .) 5) (eq (slice . 0 5) "HOME=")}}container_home={{slice . 5 | printf "%q"}}{{end}}{{end}}; {{range .Config.Env}}{{if and (ge (len .) 5) (eq (slice . 0 5) "PATH=")}}container_path={{slice . 5 | printf "%q"}}{{end}}{{end}}' \ "${container_name}")" # dry run mode, just generate the command and print it. No execution. if [ "${dryrun}" -ne 0 ]; then cmd="$(generate_enter_command | sed 's/\t//g')" printf "%s %s\n" "${cmd}" "$*" exit 0 fi # Check if the container is even there if [ "${container_status}" = "unknown" ]; then # If not, prompt to create it first # If we're not-interactive, just don't ask questions if [ "${non_interactive}" -eq 1 ]; then response="yes" else printf >&2 "Create it now, out of image %s? [Y/n]: " "${container_image_default}" read -r response response="${response:-"Y"}" fi # Accept only y,Y,Yes,yes,n,N,No,no. case "${response}" in y | Y | Yes | yes | YES) # Ok, let's create the container with just 'distrobox create $container_name create_command="$(dirname "${0}")/distrobox-create" if [ "${rootful}" -ne 0 ]; then create_command="${create_command} --root" fi create_command="${create_command} --yes -i ${container_image_default} -n ${container_name}" printf >&2 "Creating the container %s\n" "${container_name}" if [ "${dryrun}" -ne 1 ]; then ${create_command} fi ;; n | N | No | no | NO) printf >&2 "Ok. For creating it, run this command:\n" printf >&2 "\tdistrobox create --image /:\n" exit 0 ;; *) # Default case: If no more options then break out of the loop. printf >&2 "Invalid input.\n" printf >&2 "The available choices are: y,Y,Yes,yes,YES or n,N,No,no,NO.\nExiting.\n" exit 1 ;; esac fi # If the container is not already running, we need to start if first if [ "${container_status}" != "running" ]; then # If container is not running, start it first # # Here, we save the timestamp before launching the start command, so we can # be sure we're working with this very same session of logs later. log_timestamp="$(date -u +%FT%T).000000000+00:00" ${container_manager} start "${container_name}" > /dev/null # # Check if the container is going in error status earlier than the # entrypoint if [ "$(${container_manager} inspect \ --type container \ --format "{{.State.Status}}" "${container_name}")" != "running" ]; then printf >&2 "\033[31m Error: could not start entrypoint.\n\033[0m" container_manager_log="$(${container_manager} logs "${container_name}")" printf >&2 "%s\n" "${container_manager_log}" exit 1 fi printf >&2 "%-40s\t" "Starting container..." mkdir -p "${app_cache_dir}" rm -f "${app_cache_dir}/.${container_name}.fifo" mkfifo "${app_cache_dir}/.${container_name}.fifo" while true; do # Exit early in case of crashed/stopped container during setup if [ "$(${container_manager} inspect --type container --format '{{.State.Status}}' "${container_name}")" != "running" ]; then printf >&2 "\nContainer Setup Failure!\n" exit 1 fi # save starting loop timestamp in temp variable, we'll use it # after to let logs command minimize possible holes ${container_manager} logs --since "${log_timestamp}" -f "${container_name}" \ > "${app_cache_dir}/.${container_name}.fifo" 2>&1 & logs_pid="$!" # read logs from log_timestamp to now, line by line while IFS= read -r line; do case "${line}" in "+"*) # Ignoring logging commands ;; "Error:"*) printf >&2 "\033[31m %s\n\033[0m" "${line}" exit 1 ;; "Warning:"*) printf >&2 "\n\033[33m %s\033[0m" "${line}" ;; "distrobox:"*) current_line="$(echo "${line}" | cut -d' ' -f2-)" # Save current line in the status, to avoid printing the same line multiple times printf >&2 "\033[32m [ OK ]\n\033[0m%-40s\t" "${current_line}" ;; "container_setup_done"*) printf >&2 "\033[32m [ OK ]\n\033[0m" kill "${logs_pid}" > /dev/null 2>&1 break 2 ;; *) ;; esac done < "${app_cache_dir}/.${container_name}.fifo" done # cleanup fifo rm -f "${app_cache_dir}/.${container_name}.fifo" printf >&2 "\nContainer Setup Complete!\n" fi ################################################################################ # Execution section, in this section we will manipulate the positional parameters # in order to generate our long docker/podman/lilipod command to execute. # # We use positional parameters in order to have the shell manage escaping and spaces # so we remove the problem of we having to handle them. # # 1 - handle absence of custom command, we will need to add a getent command to # execute the right container's user's shell # 2 - in case of unshared groups (or initful) we need to trigger a proper login # using `su`, so we will need to manipulate these arguments accorodingly # 3 - prepend our generated command # to do this, we use `tac` so we reverse loop it and prepend each argument. # 4 - now that we're done, we can prepend our container_command # we will need to use `rev` to reverse it as we reverse loop and prepend each # argument ################################################################################ # # Setup default commands if none are specified # execute a getent command using the /bin/sh shell # to find out the default shell of the user, and # do a login shell with it (eg: /bin/bash -l) if [ "${container_custom_command}" -eq 0 ]; then set - "$@" "/bin/sh" "-c" "\$(getent passwd '${container_command_user}' | cut -f 7 -d :) -l" fi # If we have a command and we're unsharing groups, we need to execute those # command using su $container_command_user # if we're in a tty, also allocate one if [ "${unshare_groups:-0}" -eq 1 ]; then # shellcheck disable=SC2089,SC2016 set -- "-c" '"$0" "$@"' -- "$@" set -- "-s" "/bin/sh" "$@" if [ "${headless}" -eq 0 ]; then set -- "--pty" "$@" fi set -- "-m" "$@" set -- "${container_command_user}" "$@" set -- "su" "$@" fi # Generate the exec command and run it cmd="$(generate_enter_command | awk '{a[i++]=$0} END {for (j=i-1; j>=0;) print a[j--]}')" # Reverse it so we can reverse loop and prepend the command's arguments # to our positional parameters IFS=' ' for arg in ${cmd}; do set - "${arg}" "$@" done # Prepend the container manager command # reverse it first, so we can loop backward as we're prepending not appending IFS=' ' for arg in $(echo "${container_manager}" | rev); do arg="$(echo "${arg}" | rev)" set - "${arg}" "$@" done exec "$@"