Files
distrobox/distrobox-export
T
2023-09-15 10:58:15 +02:00

499 lines
15 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
# DISTROBOX_ENTER_PATH
# DISTROBOX_HOST_HOME
# Defaults
export_action=""
exported_app=""
exported_app_label=""
exported_bin=""
exported_delete=0
extra_flags=""
# Use DBX_HOST_HOME if defined, else fallback to HOME
# DBX_HOST_HOME is set in case container is created
# with custom --home directory
host_home="${DISTROBOX_HOST_HOME:-"${HOME}"}"
dest_path="${host_home}/.local/bin"
is_login=0
is_sudo=0
rootful=""
start_shell=""
verbose=0
version="1.5.0.2"
# We depend on some commands, let's be sure we have them
base_dependencies="basename find grep sed"
for dep in ${base_dependencies}; do
if ! command -v "${dep}" > /dev/null; then
printf >&2 "Missing dependency: %s\n" "${dep}"
exit 127
fi
done
# Print usage to stdout.
# Arguments:
# None
# Outputs:
# print usage with examples.
show_help() {
cat << EOF
distrobox version: ${version}
Usage:
distrobox-export --app mpv [--extra-flags "flags"] [--delete] [--sudo]
distrobox-export --bin /path/to/bin [--export-path ~/.local/bin] [--extra-flags "flags"] [--delete] [--sudo]
Options:
--app/-a: name of the application to export
--bin/-b: absolute path of the binary to export
--delete/-d: delete exported application or binary
--export-label/-el: label to add to exported application name.
Defaults to (on \$container_name)
--export-path/-ep: path where to export the binary
--extra-flags/-ef: extra flags to add to the command
--login/-l run the exported item in a login shell
--sudo/-S: specify if the exported item should be run as sudo
--help/-h: show this message
--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
;;
-V | --version)
printf "distrobox: %s\n" "${version}"
exit 0
;;
-a | --app)
if [ -n "$2" ]; then
export_action="app"
exported_app="$2"
shift
shift
fi
;;
-b | --bin)
if [ -n "$2" ]; then
export_action="bin"
exported_bin="$2"
shift
shift
fi
;;
-l | --login)
is_login=1
shift
;;
-S | --sudo)
is_sudo=1
shift
;;
-el | --export-label)
if [ -n "$2" ]; then
exported_app_label="$2"
shift
shift
fi
;;
-ep | --export-path)
if [ -n "$2" ]; then
dest_path="$2"
shift
shift
fi
;;
-ef | --extra-flags)
if [ -n "$2" ]; then
extra_flags="$2"
shift
shift
fi
;;
-d | --delete)
exported_delete=1
shift
;;
-*) # 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.
break ;;
esac
done
set -o errexit
set -o nounset
# set verbosity
if [ "${verbose}" -ne 0 ]; then
set -o xtrace
fi
# Check we're running inside a container and not on the host.
if [ ! -f /run/.containerenv ] && [ ! -f /.dockerenv ] && [ -z "${container:-}" ]; then
printf >&2 "You must run %s inside a container!\n" " $(basename "$0")"
exit 126
fi
# Check if we're in a rootful or rootless container.
if grep -q "rootless=0" /run/.containerenv 2> /dev/null; then
rootful="--root"
fi
# We're working with HOME, so we must run as USER, not as root.
if [ "$(id -u)" -eq 0 ]; then
printf >&2 "You must not run %s as root!\n" " $(basename "$0")"
exit 1
fi
# Ensure the foundamental variables are set and not empty, we will not proceed
# if they are not all set.
if [ -z "${exported_app}" ] &&
[ -z "${exported_bin}" ]; then
printf >&2 "Error: Invalid arguments.\n"
printf >&2 "Error: missing export target. Run\n"
printf >&2 "\tdistrobox-export --help\n"
printf >&2 "for more information.\n"
exit 2
fi
# Ensure we're not receiving more than one action at time.
if [ -n "${exported_app}" ] && [ -n "${exported_bin}" ]; then
printf >&2 "Error: Invalid arguments, choose only one action below.\n"
printf >&2 "Error: You can only export one thing at time.\n"
exit 2
fi
# Ensure we have the export-path set when exporting a binary.
if [ -n "${exported_bin}" ] && [ -z "${dest_path}" ]; then
printf >&2 "Error: Missing argument export-path.\n"
exit 2
fi
if [ -n "${CONTAINER_ID}" ]; then
container_name="${CONTAINER_ID}"
# if the variable is not set, let's fall back to some tricks.
elif [ -e /.dockerenv ]; then
# we're in docker, let's use this trick
container_name=$(grep "memory:/" < /proc/self/cgroup | sed 's|.*/||')
elif [ -e /run/.containerenv ]; then
# we're in podman or lilipod, use this other trick
container_name=$(grep "name=" /run/.containerenv | cut -d'=' -f2- | tr -d '"')
fi
# If one of the previous methods didn't work
# let's fallback to a more simple way but error prone
if [ -z "${container_name}" ]; then
container_name=$(uname -n | cut -d'.' -f1)
fi
#
if [ "${is_login}" -ne 0 ]; then
if [ "${is_sudo}" -ne 0 ]; then
start_shell="$(command -v sudo) -i"
else
start_shell="$(command -v sudo) -u ${USER} -i"
fi
elif [ "${is_sudo}" -ne 0 ]; then
start_shell="$(command -v sudo)"
else
start_shell=""
fi
# Prefix to add to an existing command to work through the container
container_command_prefix="${DISTROBOX_ENTER_PATH:-"distrobox-enter"} ${rootful} -n ${container_name} -- ${start_shell} "
if [ -z "${exported_app_label}" ]; then
exported_app_label=" (on ${container_name})"
fi
# Print generated script from template
# Arguments:
# none it will use the ones set up globally
# Outputs:
# print generated script.
generate_script() {
cat << EOF
#!/bin/sh
# distrobox_binary
# name: ${container_name}
if [ -z "\${CONTAINER_ID}" ]; then
exec "${DISTROBOX_ENTER_PATH:-"distrobox-enter"}" ${rootful} -n ${container_name} -- \
${start_shell} ${exported_bin} ${extra_flags} "\$@"
elif [ -n "\${CONTAINER_ID}" ] && [ "\${CONTAINER_ID}" != "${container_name}" ]; then
exec distrobox-host-exec ${dest_path}/$(basename "${exported_bin}") ${extra_flags} "\$@"
else
exec ${exported_bin} "\$@"
fi
EOF
return $?
}
# Export binary to destination directory.
# the following function will use generate_script to create a shell script in
# dest_path that will execute the exported binary in the selected distrobox.
#
# Arguments:
# none it will use the ones set up globally
# Outputs:
# a generated_script in dest_path
# or error code.
export_binary() {
# Ensure the binary we're exporting is installed
if [ ! -f "${exported_bin}" ]; then
printf >&2 "Error: cannot find %s.\n" "${exported_bin}"
return 127
fi
# generate dest_file path
dest_file="${dest_path}/$(basename "${exported_bin}")"
# If we're deleting it, just do it and exit
if [ "${exported_delete}" -ne 0 ] &&
# ensure it's a distrobox exported binary
grep -q "distrobox_binary" "${dest_file}"; then
if rm -f "${dest_file}"; then
printf "%s from %s removed successfully from %s.\nOK!\n" \
"${exported_bin}" "${container_name}" "${dest_path}"
return 0
fi
fi
# test if we have writing rights on the file
if ! touch "${dest_file}"; then
printf >&2 "Error: cannot create destination file %s.\n" "${dest_file}"
return 1
fi
# create the script from template and write to file
if generate_script > "${dest_file}"; then
chmod +x "${dest_file}"
printf "%s from %s exported successfully in %s.\nOK!\n" \
"${exported_bin}" "${container_name}" "${dest_path}"
return 0
fi
# Unknown error.
return 3
}
# Export graphical application to the host.
# the following function will scan the distrobox for desktop and icon files for
# the selected application. It will then put the needed icons in the host's icons
# directory and create a new .desktop file that will execute the selected application
# in the distrobox.
#
# Arguments:
# none it will use the ones set up globally
# Outputs:
# needed icons in /run/host/$host_home/.local/share/icons
# needed desktop files in /run/host/$host_home/.local/share/applications
# or error code.
export_application() {
canon_dirs=""
[ -d /usr/share/applications ] && canon_dirs="/usr/share/applications"
[ -d /usr/local/share/applications ] && canon_dirs="${canon_dirs} /usr/local/share/applications"
[ -d /var/lib/flatpak/exports/share/applications ] &&
canon_dirs="${canon_dirs} /var/lib/flatpak/exports/share/applications"
[ -d "${HOME}/.local/share/applications" ] && canon_dirs="${canon_dirs} ${HOME}/.local/share/applications"
# In this phase we search for applications to export.
# First find command will grep through all files in the canonical directories
# and only list files that contain the $exported_app, excluding those that
# already contains a distrobox-enter command. So skipping already exported apps.
# Second find will list all files that contain the name specified, so that
# it is possible to export an app not only by its executable name but also
# by its launcher name.
desktop_files=$(
# shellcheck disable=SC2086,SC2038
find ${canon_dirs} -type f -o -type l |
xargs -I{} grep -le "Exec=.*${exported_app}.*" -le "Name=.*${exported_app}.*" "{}" |
xargs -I{} grep -Le "Exec=.*${DISTROBOX_ENTER_PATH:-"distrobox-enter"}.*" "{}" |
xargs -I{} printf "%s@" {}
)
# Ensure the app we're exporting is installed
# Check that we found some desktop files first.
if [ -z "${desktop_files}" ]; then
printf >&2 "Error: cannot find any desktop files.\n"
printf >&2 "Error: trying to export a non-installed application.\n"
return 127
fi
# Find icons by usinc the Icon= specification. If it's only a name, we'll
# search for the file, if it's already a path, just grab it.
icon_files=""
IFS="@"
for desktop_file in ${desktop_files}; do
if [ -z "${desktop_file}" ]; then
continue
fi
icon_name="$(grep Icon= "${desktop_file}" | cut -d'=' -f2- | tr '\n' '@')"
# In case it's an hard path, conserve it and continue
if [ -e "${icon_name}" ]; then
icon_files="${icon_files}@${icon_name}"
continue
fi
for icon in ${icon_name}; do
# If it's not an hard path, find all files in the canonical paths.
icon_files="${icon_files}@$(find \
/usr/share/icons \
/usr/share/pixmaps \
/var/lib/flatpak/exports/share/icons -iname "*${icon}*" \
-printf "%p@" 2> /dev/null || :)"
done
done
# create applications dir if not existing
mkdir -p "/run/host${host_home}/.local/share/applications"
# copy icons in home directory
icon_file_absolute_path=""
IFS="@"
for icon_file in ${icon_files}; do
if [ -z "${icon_file}" ]; then
continue
fi
# replace canonical paths with equivalent paths in HOME
icon_home_directory="$(dirname "${icon_file}" |
sed "s|/usr/share/|\/run\/host\/${host_home}/.local/share/|g" |
sed "s|/var/lib/flatpak/exports/share|\/run\/host\/${host_home}/.local/share/|g" |
sed "s|pixmaps|icons|g")"
# check if we're exporting an icon which is not in a canonical path
if [ "${icon_home_directory}" = "$(dirname "${icon_file}")" ]; then
icon_home_directory="/run/host${host_home}/.local/share/icons/"
icon_file_absolute_path="${icon_home_directory}$(basename "${icon_file}")"
fi
# check if we're exporting or deleting
if [ "${exported_delete}" -ne 0 ]; then
# we need to remove, not export
rm -rf "${icon_home_directory:?}"/"$(basename "${icon_file:?}")"
continue
fi
# we wanto to export the application's icons
mkdir -p "${icon_home_directory}"
if [ ! -e "${icon_home_directory}/$(basename "${icon_file}")" ] && [ -e "$(realpath "${icon_file}")" ]; then
cp -rf "$(realpath "${icon_file}")" "${icon_home_directory}"
fi
done
# create desktop files for the distrobox
IFS="@"
for desktop_file in ${desktop_files}; do
if [ -z "${desktop_file}" ]; then
continue
fi
desktop_original_file="$(basename "${desktop_file}")"
desktop_home_file="${container_name}-$(basename "${desktop_file}")"
# check if we're exporting or deleting
if [ "${exported_delete}" -ne 0 ]; then
rm -f "/run/host${host_home}/.local/share/applications/${desktop_original_file}"
rm -f "/run/host${host_home}/.local/share/applications/${desktop_home_file}"
# we're done, go to next
continue
fi
# Add command_prefix
# Add extra flags
# Add closing quote
# If a TryExec is present, we have to fake it as it will not work
# through the container separation
sed "s|^Exec=|Exec=${container_command_prefix} |g" "${desktop_file}" |
sed "s|\(%.*\)|${extra_flags} \1|g" |
sed "/^TryExec=.*/d" |
sed "/^DBusActivatable=true/d" |
sed "s|Name.*|& ${exported_app_label}|g" \
> "/run/host${host_home}/.local/share/applications/${desktop_home_file}"
# in the end we add the final quote we've opened in the "container_command_prefix"
if ! grep -q "StartupWMClass" "/run/host${host_home}/.local/share/applications/${desktop_home_file}"; then
printf "StartupWMClass=%s\n" "${exported_app}" >> "\
/run/host${host_home}/.local/share/applications/${desktop_home_file}"
fi
# In case of an icon in a non canonical path, we need to replace the path
# in the desktop file.
if [ -n "${icon_file_absolute_path}" ]; then
sed -i "s|Icon=.*|Icon=${icon_file_absolute_path}|g" \
"/run/host${host_home}/.local/share/applications/${desktop_home_file}"
# we're done, go to next
continue
fi
# In case of an icon in a canonical path, but specified as an absolute
# we need to replace the path in the desktop file.
sed -i "s|Icon=/usr/share/|Icon=/run/host${host_home}/.local/share/|g" \
"/run/host${host_home}/.local/share/applications/${desktop_home_file}"
sed -i "s|pixmaps|icons|g" \
"/run/host${host_home}/.local/share/applications/${desktop_home_file}"
done
# Update the desktop files database to ensure exported applications will
# show up in the taskbar/desktop menu/whatnot right after running this
# script.
/usr/bin/distrobox-host-exec --yes update-desktop-database "${host_home}/.local/share/applications" 2> /dev/null || :
if [ "${exported_delete}" -ne 0 ]; then
printf "Application %s successfully un-exported.\nOK!\n" "${exported_app}"
printf "%s will disappear from your applications list in a few seconds.\n" "${exported_app}"
else
printf "Application %s successfully exported.\nOK!\n" "${exported_app}"
printf "%s will appear in your applications list in a few seconds.\n" "${exported_app}"
fi
}
# Main routine
case "${export_action}" in
app)
export_application
;;
bin)
export_binary
;;
*)
printf >&2 "Invalid arguments, choose an action below.\n"
show_help
exit 2
;;
esac