mirror of
https://github.com/89luca89/distrobox.git
synced 2026-08-19 01:14:49 -05:00
645 lines
17 KiB
Bash
Executable File
645 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/>.
|
|
|
|
# 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)"
|
|
|
|
# POSIX
|
|
#
|
|
default_input_file="./distrobox.ini"
|
|
delete=-1
|
|
distrobox_path="$(dirname "${0}")"
|
|
dryrun=0
|
|
boxname=""
|
|
input_file=""
|
|
replace=0
|
|
root_flag=""
|
|
# tmpfile will be used as a little buffer to pass variables without messing up
|
|
# quoting and escaping
|
|
tmpfile="$(mktemp -u)"
|
|
tmp_download_file="$(mktemp -u)"
|
|
verbose=0
|
|
version="1.8.1.2"
|
|
# initializing block of variables used in the manifest
|
|
additional_flags=""
|
|
additional_packages=""
|
|
entry=""
|
|
home=""
|
|
hostname=""
|
|
image=""
|
|
clone=""
|
|
init=""
|
|
init_hooks=""
|
|
nvidia=""
|
|
pre_init_hooks=""
|
|
pull=""
|
|
root=""
|
|
start_now=""
|
|
unshare_groups=""
|
|
unshare_ipc=""
|
|
unshare_netns=""
|
|
unshare_process=""
|
|
unshare_devsys=""
|
|
unshare_all=""
|
|
volume=""
|
|
exported_apps=""
|
|
exported_bins=""
|
|
exported_bins_path="${HOME}/.local/bin"
|
|
|
|
# Cleanup tmpfiles on exit
|
|
trap 'rm -f ${tmpfile} ${tmp_download_file}' EXIT
|
|
|
|
# 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}" ]; then
|
|
printf >&2 "Running %s via SUDO/DOAS is not supported." "$(basename "${0}")"
|
|
printf >&2 "Instead, please try using root=true property in the distrobox.ini file.\n"
|
|
exit 1
|
|
fi
|
|
|
|
# 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
|
|
|
|
[ -n "${DBX_VERBOSE}" ] && verbose="${DBX_VERBOSE}"
|
|
|
|
# Fixup variable=[true|false], in case we find it in the config file(s)
|
|
[ "${verbose}" = "true" ] && verbose=1
|
|
[ "${verbose}" = "false" ] && verbose=0
|
|
|
|
# show_help will print usage to stdout.
|
|
# Arguments:
|
|
# None
|
|
# Expected global variables:
|
|
# version: string distrobox version
|
|
# Expected env variables:
|
|
# None
|
|
# Outputs:
|
|
# print usage with examples.
|
|
show_help()
|
|
{
|
|
cat << EOF
|
|
distrobox version: ${version}
|
|
|
|
Usage:
|
|
|
|
distrobox assemble create
|
|
distrobox assemble rm
|
|
distrobox assemble create --file /path/to/file.ini
|
|
distrobox assemble rm --file /path/to/file.ini
|
|
distrobox assemble create --replace --file /path/to/file.ini
|
|
|
|
Options:
|
|
|
|
--file: path or URL to the distrobox manifest/ini file
|
|
--name/-n: run against a single entry in the manifest/ini file
|
|
--replace/-R: replace already existing distroboxes with matching names
|
|
--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
|
|
create)
|
|
delete=0
|
|
shift
|
|
;;
|
|
rm)
|
|
delete=1
|
|
shift
|
|
;;
|
|
--file)
|
|
# Call a "show_help" function to display a synopsis, then exit.
|
|
if [ -n "$2" ]; then
|
|
input_file="${2}"
|
|
shift
|
|
shift
|
|
fi
|
|
;;
|
|
-n | --name)
|
|
# Call a "show_help" function to display a synopsis, then exit.
|
|
if [ -n "$2" ]; then
|
|
boxname="${2}"
|
|
shift
|
|
shift
|
|
fi
|
|
;;
|
|
-h | --help)
|
|
# Call a "show_help" function to display a synopsis, then exit.
|
|
show_help
|
|
exit 0
|
|
;;
|
|
-d | --dry-run)
|
|
shift
|
|
dryrun=1
|
|
;;
|
|
-v | --verbose)
|
|
verbose=1
|
|
shift
|
|
;;
|
|
-R | --replace)
|
|
replace=1
|
|
shift
|
|
;;
|
|
-V | --version)
|
|
printf "distrobox: %s\n" "${version}"
|
|
exit 0
|
|
;;
|
|
--) # End of all options.
|
|
shift
|
|
break
|
|
;;
|
|
-*) # 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
|
|
input_file="$1"
|
|
shift
|
|
else
|
|
break
|
|
fi
|
|
;;
|
|
esac
|
|
done
|
|
|
|
set -o errexit
|
|
set -o nounset
|
|
# set verbosity
|
|
if [ "${verbose}" -ne 0 ]; then
|
|
set -o xtrace
|
|
fi
|
|
|
|
# check if we're getting the right inputs
|
|
if [ "${delete}" -eq -1 ]; then
|
|
printf >&2 "Please specify create or rm.\n"
|
|
show_help
|
|
exit 1
|
|
fi
|
|
|
|
# Fallback to distrobox.ini if no file is passed
|
|
if [ -z "${input_file}" ]; then
|
|
input_file="${default_input_file}"
|
|
fi
|
|
|
|
# Check if file effectively exists
|
|
if [ ! -e "${input_file}" ]; then
|
|
|
|
if command -v curl > /dev/null 2>&1; then
|
|
download="curl --connect-timeout 3 --retry 1 -sLo"
|
|
elif command -v wget > /dev/null 2>&1; then
|
|
download="wget --timeout=3 --tries=1 -qO"
|
|
fi
|
|
|
|
if ! ${download} - "${input_file}" > "${tmp_download_file}"; then
|
|
printf >&2 "File %s does not exist.\n" "${input_file}"
|
|
exit 1
|
|
else
|
|
input_file="${tmp_download_file}"
|
|
fi
|
|
fi
|
|
|
|
# run_distrobox will create distrobox with parameters parsed from ini file.
|
|
# Arguments:
|
|
# name: name of the distrobox.
|
|
# Expected global variables:
|
|
# boxname: string name of the target container
|
|
# tmpfile: string name of the tmpfile to read
|
|
# delete: bool delete container
|
|
# replace: bool replace container
|
|
# dryrun: bool dryrun (only print, no execute)
|
|
# verbose: bool verbose
|
|
# Expected env variables:
|
|
# None
|
|
# Outputs:
|
|
# execution of the proper distrobox-create command.
|
|
run_distrobox()
|
|
{
|
|
name="${1}"
|
|
additional_flags=""
|
|
additional_packages=""
|
|
entry=""
|
|
home=""
|
|
hostname=""
|
|
image=""
|
|
clone=""
|
|
init=""
|
|
init_hooks=""
|
|
nvidia=""
|
|
pre_init_hooks=""
|
|
pull=""
|
|
root=""
|
|
start_now=""
|
|
unshare_groups=""
|
|
unshare_ipc=""
|
|
unshare_netns=""
|
|
unshare_process=""
|
|
unshare_devsys=""
|
|
unshare_all=""
|
|
volume=""
|
|
exported_apps=""
|
|
exported_bins=""
|
|
exported_bins_path="${HOME}/.local/bin"
|
|
|
|
# Skip item if --name used and no match is found
|
|
if [ "${boxname}" != "" ] && [ "${boxname}" != "${name}" ]; then
|
|
rm -f "${tmpfile}"
|
|
return
|
|
fi
|
|
|
|
# Source the current block variables
|
|
if [ -e "${tmpfile}" ]; then
|
|
# shellcheck disable=SC1090
|
|
. "${tmpfile}" && rm -f "${tmpfile}"
|
|
fi
|
|
|
|
if [ -n "${root}" ] && [ "${root}" -eq 1 ]; then
|
|
root_flag="--root"
|
|
fi
|
|
|
|
# We're going to delete, not create!
|
|
if [ "${delete}" -ne 0 ] || [ "${replace}" -ne 0 ]; then
|
|
printf " - Deleting %s...\n" "${name}"
|
|
|
|
if [ "${dryrun}" -eq 0 ]; then
|
|
# shellcheck disable=SC2086,2248
|
|
"${distrobox_path}"/distrobox rm ${root_flag} -f "${name}" > /dev/null || :
|
|
fi
|
|
|
|
if [ "${delete}" -ne 0 ]; then
|
|
return
|
|
fi
|
|
fi
|
|
|
|
# We're going to create!
|
|
printf " - Creating %s...\n" "${name}"
|
|
|
|
# If distrobox already exist, and we have replace enabled, destroy the container
|
|
# we have to recreate it.
|
|
# shellcheck disable=SC2086,2248
|
|
if "${distrobox_path}"/distrobox-list ${root_flag} | grep -qw " ${name} " && [ "${dryrun}" -eq 0 ]; then
|
|
printf >&2 "%s already exists\n" "${name}"
|
|
return 0
|
|
fi
|
|
|
|
# Now we dynamically generate the distrobox-create command based on the
|
|
# declared flags.
|
|
result_command="${distrobox_path}/distrobox-create --yes"
|
|
if [ "${verbose}" -ne 0 ]; then
|
|
result_command="${result_command} -v"
|
|
fi
|
|
if [ -n "${name}" ]; then
|
|
result_command="${result_command} --name $(sanitize_variable "${name}")"
|
|
fi
|
|
if [ -n "${image}" ]; then
|
|
result_command="${result_command} --image $(sanitize_variable "${image}")"
|
|
fi
|
|
if [ -n "${clone}" ]; then
|
|
result_command="${result_command} --clone $(sanitize_variable "${clone}")"
|
|
fi
|
|
if [ -n "${init}" ] && [ "${init}" -eq 1 ]; then
|
|
result_command="${result_command} --init"
|
|
fi
|
|
if [ -n "${root}" ] && [ "${root}" -eq 1 ]; then
|
|
result_command="${result_command} --root"
|
|
fi
|
|
if [ -n "${pull}" ] && [ "${pull}" -eq 1 ]; then
|
|
result_command="${result_command} --pull"
|
|
fi
|
|
if [ -n "${entry}" ] && [ "${entry}" -eq 0 ]; then
|
|
result_command="${result_command} --no-entry"
|
|
fi
|
|
if [ -n "${nvidia}" ] && [ "${nvidia}" -eq 1 ]; then
|
|
result_command="${result_command} --nvidia"
|
|
fi
|
|
if [ -n "${unshare_netns}" ] && [ "${unshare_netns}" -eq 1 ]; then
|
|
result_command="${result_command} --unshare-netns"
|
|
fi
|
|
if [ -n "${unshare_groups}" ] && [ "${unshare_groups}" -eq 1 ]; then
|
|
result_command="${result_command} --unshare-groups"
|
|
fi
|
|
if [ -n "${unshare_ipc}" ] && [ "${unshare_ipc}" -eq 1 ]; then
|
|
result_command="${result_command} --unshare-ipc"
|
|
fi
|
|
if [ -n "${unshare_process}" ] && [ "${unshare_process}" -eq 1 ]; then
|
|
result_command="${result_command} --unshare-process"
|
|
fi
|
|
if [ -n "${unshare_devsys}" ] && [ "${unshare_devsys}" -eq 1 ]; then
|
|
result_command="${result_command} --unshare-devsys"
|
|
fi
|
|
if [ -n "${unshare_all}" ] && [ "${unshare_all}" -eq 1 ]; then
|
|
result_command="${result_command} --unshare-all"
|
|
fi
|
|
if [ -n "${home}" ]; then
|
|
result_command="${result_command} --home $(sanitize_variable "${home}")"
|
|
fi
|
|
if [ -n "${hostname}" ]; then
|
|
result_command="${result_command} --hostname $(sanitize_variable "${hostname}")"
|
|
fi
|
|
if [ -n "${init_hooks}" ]; then
|
|
IFS="¤"
|
|
args=": ;"
|
|
separator=""
|
|
for arg in ${init_hooks}; do
|
|
if [ -z "${arg}" ]; then
|
|
continue
|
|
fi
|
|
|
|
# Convert back from base64
|
|
arg="$(echo "${arg}" | base64 -d)"
|
|
args="${args} ${separator} ${arg}"
|
|
|
|
# Prepare for the next line, if we already have a ';' or '&&', do nothing
|
|
# else prefer adding '&&'
|
|
if ! echo "${arg}" | grep -qE ';[[:space:]]{0,1}$' &&
|
|
! echo "${arg}" | grep -qE '&&[[:space:]]{0,1}$'; then
|
|
separator="&&"
|
|
else
|
|
separator=""
|
|
fi
|
|
done
|
|
result_command="${result_command} --init-hooks $(sanitize_variable "${args}")"
|
|
fi
|
|
# Replicable flags
|
|
if [ -n "${pre_init_hooks}" ]; then
|
|
IFS="¤"
|
|
args=": ;"
|
|
separator=""
|
|
for arg in ${pre_init_hooks}; do
|
|
if [ -z "${arg}" ]; then
|
|
continue
|
|
fi
|
|
|
|
# Convert back from base64
|
|
arg="$(echo "${arg}" | base64 -d)"
|
|
args="${args} ${separator} ${arg}"
|
|
|
|
# Prepare for the next line, if we already have a ';' or '&&', do nothing
|
|
# else prefer adding '&&'
|
|
if ! echo "${arg}" | grep -qE ';[[:space:]]{0,1}$' &&
|
|
! echo "${arg}" | grep -qE '&&[[:space:]]{0,1}$'; then
|
|
separator="&&"
|
|
else
|
|
separator=""
|
|
fi
|
|
done
|
|
result_command="${result_command} --pre-init-hooks $(sanitize_variable "${args}")"
|
|
fi
|
|
if [ -n "${additional_packages}" ]; then
|
|
IFS="¤"
|
|
args=""
|
|
for packages in ${additional_packages}; do
|
|
if [ -z "${packages}" ]; then
|
|
continue
|
|
fi
|
|
args="${args} ${packages}"
|
|
done
|
|
result_command="${result_command} --additional-packages $(sanitize_variable "${args}")"
|
|
fi
|
|
if [ -n "${volume}" ]; then
|
|
IFS="¤"
|
|
for vol in ${volume}; do
|
|
if [ -z "${vol}" ]; then
|
|
continue
|
|
fi
|
|
result_command="${result_command} --volume $(sanitize_variable "${vol}")"
|
|
done
|
|
fi
|
|
if [ -n "${additional_flags}" ]; then
|
|
IFS="¤"
|
|
for flag in ${additional_flags}; do
|
|
if [ -z "${flag}" ]; then
|
|
continue
|
|
fi
|
|
result_command="${result_command} --additional-flags $(sanitize_variable "${flag}")"
|
|
done
|
|
fi
|
|
|
|
# Execute the distrobox-create command
|
|
if [ "${dryrun}" -ne 0 ]; then
|
|
echo "${result_command}"
|
|
return
|
|
fi
|
|
eval "${result_command}"
|
|
|
|
# If we need to start immediately, do it, so that the container
|
|
# is ready to be entered.
|
|
if [ -n "${start_now}" ] && [ "${start_now}" -eq 1 ]; then
|
|
# shellcheck disable=SC2086,2248
|
|
"${distrobox_path}"/distrobox enter ${root_flag} "${name}" -- touch /dev/null
|
|
fi
|
|
|
|
# if there are exported bins and apps declared, let's export them
|
|
if [ -n "${exported_apps}" ] || [ -n "${exported_bins}" ]; then
|
|
# First we start the container
|
|
# shellcheck disable=SC2086,2248
|
|
"${distrobox_path}"/distrobox enter ${root_flag} "${name}" -- touch /dev/null
|
|
|
|
IFS="¤"
|
|
for apps in ${exported_apps}; do
|
|
# Split the string by spaces
|
|
IFS=" "
|
|
for app in ${apps}; do
|
|
# Export the app
|
|
# shellcheck disable=SC2086,2248
|
|
"${distrobox_path}"/distrobox enter ${root_flag} "${name}" -- distrobox-export --app "${app}"
|
|
done
|
|
done
|
|
|
|
IFS="¤"
|
|
for bins in ${exported_bins}; do
|
|
# Split the string by spaces
|
|
IFS=" "
|
|
for bin in ${bins}; do
|
|
# Export the bin
|
|
# shellcheck disable=SC2086,2248
|
|
"${distrobox_path}"/distrobox enter ${root_flag} "${name}" -- distrobox-export --bin "${bin}" --export-path "${exported_bins_path}"
|
|
done
|
|
done
|
|
fi
|
|
}
|
|
|
|
# encode_variable will encode an input in base64, removing surrounding single/double quotes.
|
|
# Arguments:
|
|
# variable: string
|
|
# Expected global variables:
|
|
# None
|
|
# Expected env variables:
|
|
# None
|
|
# Outputs:
|
|
# a value string encoded in base64
|
|
encode_variable()
|
|
{
|
|
variable="${1}"
|
|
# remove surrounding quotes possibly added by the user
|
|
if echo "${variable}" | grep -qE '^"'; then
|
|
variable="$(echo "${variable}" | sed -e 's/^"//' -e 's/"$//')"
|
|
elif echo "${variable}" | grep -qE "^'"; then
|
|
variable="$(echo "${variable}" | sed -e "s/^'//" -e "s/'$//")"
|
|
fi
|
|
|
|
echo "${variable}" | base64 -w 0
|
|
}
|
|
|
|
# sanitize_variable will sanitize an input, add single/double quotes and escapes
|
|
# Arguments:
|
|
# variable: string
|
|
# Expected global variables:
|
|
# None
|
|
# Expected env variables:
|
|
# None
|
|
# Outputs:
|
|
# a value string sanitized
|
|
sanitize_variable()
|
|
{
|
|
variable="${1}"
|
|
|
|
# If there are spaces but no quotes, let's add them
|
|
if echo "${variable}" | grep -q " " &&
|
|
! echo "${variable}" | grep -Eq "^'|^\""; then
|
|
|
|
# if we have double quotes we should wrap the whole line in single quotes
|
|
# in order to not "undo" them
|
|
if echo "${variable}" | grep -q '"'; then
|
|
variable="'${variable}'"
|
|
else
|
|
variable="\"${variable}\""
|
|
fi
|
|
fi
|
|
|
|
# Return
|
|
echo "${variable}"
|
|
}
|
|
|
|
# parse_file will read and parse input file and call distrobox-create accordingly
|
|
# Arguments:
|
|
# file: string path of the manifest file to parse
|
|
# Expected global variables:
|
|
# tmpfile: string name of the tmpfile to read
|
|
# Expected env variables:
|
|
# None
|
|
# Outputs:
|
|
# None
|
|
parse_file()
|
|
{
|
|
file="${1}"
|
|
name=""
|
|
|
|
IFS='
|
|
'
|
|
while read -r line; do
|
|
# Remove comments and trailing spaces
|
|
line="$(echo "${line}" |
|
|
sed 's/\t/ /g' |
|
|
sed 's/^#.*//g' |
|
|
sed 's/].*#.*//g' |
|
|
sed 's/ #.*//g' |
|
|
sed 's/\s*$//g')"
|
|
|
|
if [ -z "${line}" ]; then
|
|
# blank line, skip
|
|
continue
|
|
fi
|
|
|
|
# Detect start of new section
|
|
if [ "$(echo "${line}" | cut -c 1)" = '[' ]; then
|
|
# We're starting a new section
|
|
if [ -n "${name}" ]; then
|
|
# We've finished the previous section, so this is the time to
|
|
# perform the distrobox command, before going to the new section.
|
|
run_distrobox "${name}"
|
|
fi
|
|
|
|
# Remove brackets and spaces
|
|
name="$(echo "${line}" | tr -d '][ ')"
|
|
continue
|
|
fi
|
|
|
|
# Get key-values from the file
|
|
key="$(echo "${line}" | cut -d'=' -f1 | tr -d ' ')"
|
|
value="$(echo "${line}" | cut -d'=' -f2-)"
|
|
|
|
# Normalize true|false to 0|1
|
|
[ "${value}" = "true" ] && value=1
|
|
[ "${value}" = "false" ] && value=0
|
|
|
|
# Sanitize value, by whitespaces, quotes and escapes
|
|
if [ "${key}" = "init_hooks" ] || [ "${key}" = "pre_init_hooks" ]; then
|
|
# in case of shell commands (so the hooks) we prefer to pass the variable
|
|
# around encoded, so that we don't accidentally execute stuff
|
|
# and, we will execute sanitize_variable on the final string flag at the
|
|
# end, instead of key/value base.
|
|
value="$(encode_variable "${value}")"
|
|
else
|
|
value="$(sanitize_variable "${value}")"
|
|
fi
|
|
|
|
# Save options to tempfile, to source it later
|
|
touch "${tmpfile}"
|
|
if [ -n "${key}" ] && [ -n "${value}" ]; then
|
|
if grep -q "^${key}=" "${tmpfile}"; then
|
|
# make keys cumulative
|
|
value="\${${key}}¤${value}"
|
|
fi
|
|
echo "${key}=${value}" >> "${tmpfile}"
|
|
fi
|
|
done < "${file}"
|
|
# Execute now one last time for the last block
|
|
run_distrobox "${name}"
|
|
}
|
|
|
|
# Exec
|
|
parse_file "${input_file}"
|