first commit

This commit is contained in:
2026-02-13 04:20:30 +01:00
commit 2bd6b181f4
159 changed files with 194785 additions and 0 deletions

View File

@@ -0,0 +1,793 @@
#!/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.2.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
# Here we execute the `distrobox enter` command with a `/dev/null` stdin.
# This is due to the fact that this command is very likely to be executed inside
# the read loop in the prse_file function. This way we avoid stdin to be swallowed by
# this command execution. This is valid for all the `distrobox enter` calls from now on.
#
# shellcheck disable=SC2086,2248
"${distrobox_path}"/distrobox enter ${root_flag} "${name}" -- touch /dev/null < /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 < /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}" < /dev/null
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}" < /dev/null
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}"
}
# read_section reads the content of a section from a TOML file.
# Arguments:
# section: Name of the section to find (string).
# file: Path to the file to search into (string).
# Expected global variables:
# None.
# Expected env variables:
# None.
# Outputs:
# Writes the found section body to stdout.
# Exit behavior / errors:
# Does not exit nonzero on missing section; caller should treat empty output as needed.
read_section()
{
section="$1"
file="$2"
awk -v sec="[${section}]" '
$0 == sec {show=1; next}
show && /^\[/ {exit}
show && NF {print}
' "${file}"
}
# resolve_includes Resolve 'include' keys in a manifest by inlining referenced sections.
# Arguments:
# input_file: Path to the original manifest file that may contain 'include' keys.
# output_file: Path to the file where the resolved manifest will be written (if empty, a temp file is used).
# Expected global variables:
# read_section (function) - used to extract referenced section bodies from input_file.
# replace_line (function) - used to replace include lines with extracted content.
# Expected env variables:
# TMPDIR (optional) - may influence mktemp location.
# Outputs:
# Prints the path to the output file to stdout when completed.
# Exit behavior / errors:
# Exits with status 1 and writes to stderr on missing definitions, circular includes, or helper failures.
resolve_includes()
{
input_file="$1"
output_file="$2"
include_stack=""
# At the starting point, the output file is equal to input_file
# Later on, the output file will be edited as includes will be resolved
cat "${input_file}" > "${output_file}"
n=0
while true; do
total_lines=$(wc -l < "${output_file}")
[ "${n}" -gt "${total_lines}" ] && break
line=$(sed -n "$((n + 1))p" "${output_file}")
# Detected begin of a section: clear the include stack and go on
if [ "$(echo "${line}" | cut -c 1)" = '[' ]; then
include_stack=""
n=$((n + 1))
continue
fi
# Match key=value from the current line
key="$(echo "${line}" | cut -d'=' -f1 | tr -d ' ')"
value="$(echo "${line}" | cut -d'=' -f2-)"
if [ "${key}" = "include" ]; then
# Detect circular references while including other distrobox definitions
# The reference stack is handled as a string of shape [name].[name].[name]
if expr "${include_stack}" : ".*\[${value}\]" > /dev/null; then
printf >&2 "ERROR circular reference detected: including [%s] again after %s\n" "${value}" "${include_stack}"
exit 1
else
include_stack="[${value}]¤${include_stack}"
fi
# Read the definition for the distrobox to include from the original file
# and replace the current line with the found lines.
# The line counter is not incremented to allow recursive include resolution
inc=$(read_section "$(echo "${value}" | tr -d '"')" "${input_file}")
if [ -z "${inc}" ]; then
printf >&2 "ERROR cannot include '%s': definition not found\n" "${value}"
exit 1
fi
l=$((n + 1))
replace_line "${l}" "${inc}" "${output_file}" "${output_file}" > /dev/null
continue
fi
# Nothing to do, increment counter and go on
n=$((n + 1))
done
echo "${output_file}"
}
# replace_line Replace a 1-based numbered line in a file with provided (possibly multiline) text.
# Arguments:
# line_number: 1-based index of the line to replace.
# new_value: String to insert (may contain newlines).
# input_file: Path to the original file to read from.
# output_file: Path to write the resulting file (if empty, a temp file will be created).
# Expected global variables:
# None.
# Expected env variables:
# TMPDIR (optional) - may influence mktemp location.
# Outputs:
# Prints the path to the output file to stdout when complete.
# Exit behavior / errors:
# Exits with status 1 on fatal errors (e.g., mktemp failure).
replace_line()
{
line_number="$1"
new_value="$2"
input_file="$3"
output_file="$4"
# if no output file, use a temp file
if [ -z "${output_file}" ]; then
output_file=$(mktemp -u)
fi
tmpfile=$(mktemp) || exit 1
# Split the file into two parts around the line to replace and combine with new_value
# Print lines before line_number
sed "$((line_number - 1))q" "${input_file}" > "${tmpfile}"
# Append the new_value
printf "%s\n" "${new_value}" >> "${tmpfile}"
# Append lines after line_number
sed -n "$((line_number + 1)),\$p" "${input_file}" >> "${tmpfile}"
# Replace original file with tmpfile
mv "${tmpfile}" "${output_file}"
echo "${output_file}"
}
# Exec
expanded_file=$(mktemp -u)
resolve_includes "${input_file}" "${expanded_file}"
parse_file "${expanded_file}"