#!/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 . # 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 non‑zero 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}"