mirror of
https://github.com/89luca89/distrobox.git
synced 2026-08-19 01:14:49 -05:00
526 lines
17 KiB
Bash
Executable File
526 lines
17 KiB
Bash
Executable File
#!/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 <http://www.gnu.org/licenses/>.
|
|
|
|
# POSIX
|
|
# Expected env variables:
|
|
# HOME
|
|
# USER
|
|
# Optional env variables:
|
|
# DBX_CONTAINER_IMAGE
|
|
# DBX_CONTAINER_NAME
|
|
# DBX_NON_INTERACTIVE
|
|
|
|
trap '[ "$?" -ne 0 ] && printf "\nAn error occurred\n"' EXIT
|
|
|
|
# Dont' run this command as sudo.
|
|
if [ "$(id -u)" -eq 0 ]; then
|
|
printf >&2 "Running %s as sudo is not supported.\n" "${0}"
|
|
printf >&2 "Please check the documentation on:\n"
|
|
printf >&2 "\tman distrobox-compatibility\t"
|
|
printf >&2 "or consult the documentation page on:\n"
|
|
printf >&2 "\thttps://github.com/89luca89/distrobox/blob/main/docs/compatibility.md\n"
|
|
exit 1
|
|
fi
|
|
|
|
# Defaults
|
|
container_clone=""
|
|
container_image="${DBX_CONTAINER_IMAGE:-""}"
|
|
container_image_default="registry.fedoraproject.org/fedora-toolbox:35"
|
|
container_init_hook=""
|
|
container_manager_additional_flags=""
|
|
container_name="${DBX_CONTAINER_NAME:-""}"
|
|
container_user_custom_home=""
|
|
container_user_gid="$(id -rg)"
|
|
container_user_home="${HOME:-"/"}"
|
|
container_user_name="${USER}"
|
|
container_user_uid="$(id -ru)"
|
|
# 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_entrypoint_path="$(cd "$(dirname "${0}")" && pwd)/distrobox-init"
|
|
distrobox_export_path="$(cd "$(dirname "${0}")" && pwd)/distrobox-export"
|
|
# In case init or export are not in the same path as create, let's search
|
|
# in PATH for them.
|
|
[ ! -e "${distrobox_entrypoint_path}" ] && distrobox_entrypoint_path="$(command -v distrobox-init)"
|
|
[ ! -e "${distrobox_export_path}" ] && distrobox_export_path="$(command -v distrobox-export)"
|
|
non_interactive="${DBX_NON_INTERACTIVE:-0}"
|
|
dryrun=0
|
|
init=0
|
|
verbose=0
|
|
version="1.2.14"
|
|
|
|
# Print usage to stdout.
|
|
# Arguments:
|
|
# None
|
|
# Outputs:
|
|
# print usage with examples.
|
|
show_help() {
|
|
cat <<EOF
|
|
distrobox version: ${version}
|
|
|
|
Usage:
|
|
|
|
distrobox-create --image registry.fedoraproject.org/fedora-toolbox:35 --name fedora-toolbox-35
|
|
distrobox-create --clone fedora-toolbox-35 --name fedora-toolbox-35-copy
|
|
distrobox-create --image alpine my-alpine-container
|
|
distrobox create --image fedora:35 --name test --volume /opt/my-dir:/usr/local/my-dir:rw --additional-flags "--pids-limit -1"
|
|
distrobox create --image fedora:35 --name test --additional-flags "--env MY_VAR-value"
|
|
distrobox create --image alpine:latest --name test --init-hooks "touch /var/tmp/test1 && touch /var/tmp/test2"
|
|
distrobox create -i docker.io/almalinux/8-init --init --name test
|
|
|
|
DBX_NON_INTERACTIVE=1 DBX_CONTAINER_NAME=test-alpine DBX_CONTAINER_IMAGE=alpine distrobox-create
|
|
|
|
Options:
|
|
|
|
--image/-i: image to use for the container default: registry.fedoraproject.org/fedora-toolbox:35
|
|
--name/-n: name for the distrobox default: fedora-toolbox-35
|
|
--yes/-Y: non-interactive, pull images without asking
|
|
--clone/-c: name of the distrobox container to use as base for a new container
|
|
this will be useful to either rename an existing distrobox or have multiple copies
|
|
of the same environment.
|
|
--home/-H select a custom HOME directory for the container. Useful to avoid host's home littering with temp files.
|
|
--volume additional volumes to add to the container
|
|
--additional-flags/-a: additional flags to pass to the container manager command
|
|
--init-hooks additional commands to execute during container initialization
|
|
--init/-I use init system (like systemd) inside the container.
|
|
this will make host's processes not visible from within the container.
|
|
--help/-h: show this message
|
|
--dry-run/-d: only print the container manager command generated
|
|
--verbose/-v: show more verbosity
|
|
--version/-V: show version
|
|
|
|
Compatibility:
|
|
|
|
for a list of compatible images and container managers, please consult the man page:
|
|
man distrobox-compatibility
|
|
or consult the documentation page on: https://github.com/89luca89/distrobox/blob/main/docs/compatibility.md
|
|
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)
|
|
verbose=1
|
|
shift
|
|
;;
|
|
-V | --version)
|
|
printf "distrobox: %s\n" "${version}"
|
|
exit 0
|
|
;;
|
|
-d | --dry-run)
|
|
shift
|
|
dryrun=1
|
|
;;
|
|
-I | --init)
|
|
shift
|
|
init=1
|
|
;;
|
|
-i | --image)
|
|
if [ -n "$2" ]; then
|
|
container_image="$2"
|
|
shift
|
|
shift
|
|
fi
|
|
;;
|
|
-n | --name)
|
|
if [ -n "$2" ]; then
|
|
container_name="$2"
|
|
shift
|
|
shift
|
|
fi
|
|
;;
|
|
-c | --clone)
|
|
if [ -n "$2" ]; then
|
|
container_clone="$2"
|
|
shift
|
|
shift
|
|
fi
|
|
;;
|
|
-H | --home)
|
|
if [ -n "$2" ]; then
|
|
container_user_custom_home="$2"
|
|
shift
|
|
shift
|
|
fi
|
|
;;
|
|
-Y | --yes)
|
|
non_interactive=1
|
|
shift
|
|
;;
|
|
--volume)
|
|
if [ -n "$2" ]; then
|
|
container_manager_additional_flags="${container_manager_additional_flags} ${1} ${2}"
|
|
shift
|
|
shift
|
|
fi
|
|
;;
|
|
-a | --additional-flags)
|
|
if [ -n "$2" ]; then
|
|
container_manager_additional_flags="${container_manager_additional_flags} ${2}"
|
|
shift
|
|
shift
|
|
fi
|
|
;;
|
|
--init-hooks)
|
|
if [ -n "$2" ]; then
|
|
container_init_hook="$2"
|
|
shift
|
|
shift
|
|
fi
|
|
;;
|
|
--) # End of all options.
|
|
shift
|
|
break
|
|
;;
|
|
*) # 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 [ -z "${container_name}" ] && [ -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
|
|
|
|
# We cannot have both a clone AND an image name.
|
|
if [ -n "${container_clone}" ] && [ -n "${container_image}" ]; then
|
|
printf >&2 "Error: Invalid arguments, choose only one between clone or image name.\n"
|
|
exit 2
|
|
fi
|
|
|
|
# If no clone option and no container image, let's choose a default image to use.
|
|
# Fedora toolbox is a sensitive default
|
|
if [ -z "${container_clone}" ] && [ -z "${container_image}" ]; then
|
|
container_image="${container_image_default}"
|
|
fi
|
|
|
|
# If no container_name is declared, we build our container name starting from the
|
|
# container image specified.
|
|
#
|
|
# Examples:
|
|
# alpine -> alpine
|
|
# ubuntu:20.04 -> ubuntu-20.04
|
|
# registry.fedoraproject.org/fedora-toolbox:35 -> fedora-toolbox-35
|
|
# ghcr.io/void-linux/void-linux:latest-full-x86_64 -> void-linux-latest-full-x86_64
|
|
if [ -z "${container_name}" ]; then
|
|
container_name="$(basename "${container_image}" | sed -E 's/[:.]/-/g')"
|
|
fi
|
|
|
|
# We depend on a container manager let's be sure we have it
|
|
# First we use podman, else docker
|
|
container_manager="podman"
|
|
# Be sure we have a container manager to work with.
|
|
if ! command -v "${container_manager}" >/dev/null; then
|
|
# If no podman, try docker.
|
|
container_manager="docker"
|
|
if ! command -v docker >/dev/null; then
|
|
# Error: we need at least one between docker or podman.
|
|
printf >&2 "Missing dependency: we need a container manager.\n"
|
|
printf >&2 "Please install one of podman or docker.\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"
|
|
if [ "${dryrun}" -eq 0 ]; then
|
|
exit 127
|
|
fi
|
|
fi
|
|
fi
|
|
# add verbose if -v is specified
|
|
if [ "${verbose}" -ne 0 ]; then
|
|
container_manager="${container_manager} --log-level debug"
|
|
fi
|
|
|
|
# Clone a container as a snapshot.
|
|
# Arguments:
|
|
# None
|
|
# Outputs:
|
|
# prints the image name of the newly cloned container
|
|
clone_container() {
|
|
# We need to clone a container.
|
|
# to do this we will commit the container and create a new tag. Then use it
|
|
# as image for the new container.
|
|
#
|
|
# to perform this we first ensure the source container exists and that the
|
|
# source container is stopped, else the clone will not work,
|
|
container_source_status="$(${container_manager} inspect --type container \
|
|
"${container_clone}" --format '{{.State.Status}}')"
|
|
# If the container is not already running, we need to start if first
|
|
if [ "${container_source_status}" = "running" ]; then
|
|
printf >&2 "Container %s is running.\nPlease stop it first.\n" "${container_clone}"
|
|
printf >&2 "Cannot clone a running container.\n"
|
|
return 1
|
|
fi
|
|
|
|
# Now we can extract the container ID and commit it to use as source image
|
|
# for the new container.
|
|
container_source_id="$(${container_manager} inspect --type container \
|
|
"${container_clone}" --format '{{.Id}}')"
|
|
container_commit_tag="${container_clone}:$(date +%F)"
|
|
|
|
# Commit current container state to a new image tag
|
|
printf >&2 "Duplicating %s...\n" "${container_clone}"
|
|
if ! ${container_manager} container commit \
|
|
"${container_source_id}" "${container_commit_tag}" >/dev/null; then
|
|
|
|
printf >&2 "Cannot clone container: %s\n" "${container_clone}"
|
|
return 1
|
|
fi
|
|
|
|
# Return the image tag to use for the new container creation.
|
|
printf "%s" "${container_commit_tag}"
|
|
return 0
|
|
|
|
}
|
|
|
|
# Generate Podman or Docker command to execute.
|
|
# Arguments:
|
|
# None
|
|
# Outputs:
|
|
# prints the podman or docker command to create the distrobox container
|
|
generate_command() {
|
|
# Set the container hostname the same as the container name.
|
|
result_command="${container_manager} create"
|
|
# use the host's namespace for ipc, network, pid, ulimit
|
|
result_command="${result_command}
|
|
--hostname \"${container_name}.$(uname -n)\"
|
|
--ipc host
|
|
--name \"${container_name}\"
|
|
--network host
|
|
--privileged
|
|
--security-opt label=disable
|
|
--user root:root"
|
|
|
|
if [ "${init}" -eq 0 ]; then
|
|
result_command="${result_command}
|
|
--pid host"
|
|
fi
|
|
# Mount useful stuff inside the container.
|
|
# We also mount host's root filesystem to /run/host, to be able to syphon
|
|
# dynamic configurations from the host.
|
|
#
|
|
# Mount user home, dev and host's root inside container.
|
|
# This grants access to external devices like usb webcams, disks and so on.
|
|
#
|
|
# Mount also the distrobox-init utility as the container entrypoint.
|
|
result_command="${result_command}
|
|
--env \"SHELL=${SHELL:-"/bin/bash"}\"
|
|
--env \"HOME=${container_user_home}\"
|
|
--volume \"${container_user_home}\":\"${container_user_home}\":rslave
|
|
--volume \"${distrobox_entrypoint_path}\":/usr/bin/entrypoint:ro
|
|
--volume \"${distrobox_export_path}\":/usr/bin/distrobox-export:ro
|
|
--volume /:/run/host:rslave
|
|
--volume /dev:/dev:rslave
|
|
--volume /sys:/sys:rslave
|
|
--volume /tmp:/tmp:rslave"
|
|
|
|
# This fix is needed as on Selinux systems, the host's selinux sysfs directory
|
|
# will be mounted inside the rootless container.
|
|
#
|
|
# This works around this and allows the rootless container to work when selinux
|
|
# policies are installed inside it.
|
|
#
|
|
# Ref. Podman issue 4452:
|
|
# https://github.com/containers/podman/issues/4452
|
|
if [ -e "/sys/fs/selinux" ]; then
|
|
result_command="${result_command}
|
|
--volume /sys/fs/selinux"
|
|
fi
|
|
|
|
# In some systems, for example using sysvinit, /dev/shm is a symlink
|
|
# to /run/shm, instead of the other way around.
|
|
# Resolve this detecting if /dev/shm is a symlink and mount original
|
|
# source also in the container.
|
|
# shellcheck disable=SC2312
|
|
if [ -L "/dev/shm" ]; then
|
|
result_command="${result_command}
|
|
--volume $(realpath /dev/shm):$(realpath /dev/shm)"
|
|
fi
|
|
|
|
# If you are using NixOS, or have Nix installed, /nix is a volume containing
|
|
# you binaries and many configs.
|
|
# /nix needs to be mounted if you want to execute those binaries from within
|
|
# the container. Therefore we need to mount /nix as a volume, but only if it exists.
|
|
if [ -d "/nix" ]; then
|
|
result_command="${result_command}
|
|
--volume /nix:/nix"
|
|
fi
|
|
|
|
# If we have a custom home to use,
|
|
# 1- override the HOME env variable
|
|
# 2- expor the DISTROBOX_HOST_HOME env variable pointing to original HOME
|
|
# 3- mount the custom home inside the container.
|
|
if [ -n "${container_user_custom_home}" ]; then
|
|
result_command="${result_command}
|
|
--env \"HOME=${container_user_custom_home}\"
|
|
--env \"DISTROBOX_HOST_HOME=${container_user_home}\"
|
|
--volume ${container_user_custom_home}:${container_user_custom_home}:rslave"
|
|
fi
|
|
|
|
# Mount also the /var/home dir on ostree based systems
|
|
# do this only if $HOME was not already set to /var/home/username
|
|
if [ "${container_user_home}" != "/var/home/${container_user_name}" ] &&
|
|
[ -d "/var/home/${container_user_name}" ]; then
|
|
|
|
result_command="${result_command}
|
|
--volume \"/var/home/${container_user_name}\":\"/var/home/${container_user_name}\":rslave"
|
|
fi
|
|
|
|
# Mount also the XDG_RUNTIME_DIR to ensure functionality of the apps.
|
|
if [ -d "/run/user/${container_user_uid}" ]; then
|
|
result_command="${result_command}
|
|
--volume /run/user/${container_user_uid}:/run/user/${container_user_uid}:rslave"
|
|
fi
|
|
|
|
# These are dynamic configs needed by the container to function properly
|
|
# and integrate with the host
|
|
#
|
|
# We're doing this now instead of inside the init because some distros will
|
|
# have symlinks places for these files that use absolute paths instead of
|
|
# relative paths. Those symlinks will result broken inside the container so
|
|
# we need to resolve them now on the host.
|
|
host_links="/etc/host.conf /etc/hosts /etc/resolv.conf /etc/localtime"
|
|
for host_link in ${host_links}; do
|
|
# Check if the file exists first
|
|
if [ -f "${host_link}" ] && [ -r "${host_link}" ]; then
|
|
result_command="${result_command}
|
|
--volume \"$(realpath "${host_link}")\":\"${host_link}\":ro"
|
|
fi
|
|
done
|
|
|
|
# These flags are not supported by docker, so we use them only if our
|
|
# container manager is podman.
|
|
if [ -z "${container_manager#*podman*}" ]; then
|
|
result_command="${result_command}
|
|
--userns keep-id
|
|
--ulimit host
|
|
--annotation run.oci.keep_original_groups=1
|
|
--mount type=devpts,destination=/dev/pts"
|
|
fi
|
|
|
|
# Add additional flags
|
|
result_command="${result_command} ${container_manager_additional_flags}"
|
|
|
|
# Now execute the entrypoint, refer to `distrobox-init -h` for instructions
|
|
result_command="${result_command} ${container_image}
|
|
/usr/bin/entrypoint -v --name \"${container_user_name}\"
|
|
--user ${container_user_uid}
|
|
--group ${container_user_gid}
|
|
--home \"${container_user_custom_home:-"${container_user_home}"}\"
|
|
--init \"${init}\"
|
|
-- '${container_init_hook}'
|
|
"
|
|
# use container_user_custom_home if defined, else fallback to normal home.
|
|
|
|
# Return generated command.
|
|
printf "%s" "${result_command}"
|
|
}
|
|
|
|
# Check that we have a complete distrobox installation or
|
|
# entrypoint and export will not work.
|
|
if [ -z "${distrobox_entrypoint_path}" ] || [ -z "${distrobox_export_path}" ]; then
|
|
printf >&2 "Error: no distrobox-init found in %s\n" "${PATH}"
|
|
exit 127
|
|
fi
|
|
|
|
# dry run mode, just generate the command and print it. No creation.
|
|
if [ "${dryrun}" -ne 0 ]; then
|
|
if [ -n "${container_clone}" ]; then
|
|
container_image="${container_clone}"
|
|
fi
|
|
cmd="$(generate_command)"
|
|
cmd="$(echo "${cmd}" | tr '[:blank:]\n' ' ' | tr -s ' ')"
|
|
printf "%s\n" "${cmd}"
|
|
exit 0
|
|
fi
|
|
|
|
# Check if the container already exists.
|
|
# If it does, notify the user and exit.
|
|
if ${container_manager} inspect --type container "${container_name}" >/dev/null 2>&1; then
|
|
printf "Distrobox named '%s' already exists.\n" "${container_name}"
|
|
printf "To enter, run:\n"
|
|
printf "\tdistrobox-enter --name %s\n" "${container_name}"
|
|
exit 0
|
|
fi
|
|
|
|
# if we are using the clone flag, let's set the image variable
|
|
# to the output of container duplication
|
|
if [ -n "${container_clone}" ]; then
|
|
container_image="$(clone_container)"
|
|
fi
|
|
# First, check if the image exists in the host.
|
|
# If not prompt to download it.
|
|
if ! ${container_manager} inspect --type image "${container_image}" >/dev/null 2>&1; then
|
|
if [ "${non_interactive}" -eq 0 ]; then
|
|
# Prompt to download it.
|
|
printf >&2 "Image %s not found.\n" "${container_image}"
|
|
printf >&2 "Do you want to pull the image now? [Y/n]: "
|
|
read -r response
|
|
response="${response:-"Y"}"
|
|
else
|
|
response="yes"
|
|
fi
|
|
|
|
# Accept only y,Y,Yes,yes,n,N,No,no.
|
|
case "${response}" in
|
|
y | Y | Yes | yes | YES)
|
|
# Pull the image
|
|
${container_manager} pull "${container_image}"
|
|
;;
|
|
n | N | No | no | NO)
|
|
printf >&2 "next time, run this command first:\n"
|
|
printf >&2 "\t%s pull %s\n" "${container_manager}" "${container_image}"
|
|
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
|
|
|
|
# Generate the create command and run it
|
|
cmd="$(generate_command)"
|
|
# Eval the generated command. If successful display an helpful message.
|
|
# shellcheck disable=SC2086
|
|
if eval ${cmd}; then
|
|
printf "Distrobox '%s' successfully created.\n" "${container_name}"
|
|
printf "To enter, run:\n"
|
|
printf "\tdistrobox-enter %s\n" "${container_name}"
|
|
fi
|