Files
distrobox/distrobox-export
T
2021-12-21 18:28:04 +01:00

460 lines
14 KiB
Bash
Executable File

#!/bin/sh
# POSIX
# Expected env variables:
# HOME
# USER
# DISTROBOX_ENTER_PATH
trap '[ "$?" -ne 0 ] && printf "\nAn error occurred\n"' EXIT
# Defaults
export_action=""
exported_app=""
exported_bin=""
exported_delete=0
exported_service=""
extra_flags=""
is_sudo=""
verbose=0
version="1.2.2"
# We depend on some commands, let's be sure we have them
base_dependencies="basename grep sed find"
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}
distrobox-export takes care of exporting an app a binary or a service from the container
to the host.
The exported app will be easily available in your normal launcher and it will
automatically be launched from the container it is exported from.
The exported services will be available in the host's user's systemd session, so
systemctl --user status exported_service_name
will show the status of the service exported.
The exported binaries will be exported in the "--export-path" of choice as a wrapper
script that acts naturally both on the host and in the container.
Note that "--export-path" is NOT OPTIONAL, you have to explicitly set it.
You can specify additional flags to add to the command, for example if you want
to export an electron app, you could add the "--foreground" flag to the command:
distrobox-export --app atom --extra-flags "--foreground"
distrobox-export --bin /usr/bin/vim --export-path ~/.local/bin --extra-flags "-p"
distrobox-export --service syncthing --extra-flags "-allow-newer-config"
This works for services, binaries, and apps.
Extra flags are only used then the exported app, binary, or service is used from
the host, using them inside the container will not include them.
The option "--delete" will un-export an app, binary, or service.
distrobox-export --app atom --delete
distrobox-export --bin /usr/bin/vim --export-path ~/.local/bin --delete
distrobox-export --service syncthing --delete
distrobox-export --service nginx --delete
The option "--sudo" will launch the exported item as root inside the distrobox.
Note you can use --app OR --bin OR --service but not together.
distrobox-export --service nginx --sudo
Usage:
distrobox-export --app mpv [--extra-flags "flags"] [--delete] [--sudo]
distrobox-export --service syncthing [--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
--service/-s: name of the service to export
--delete/-d: delete exported application or service
--export-path/-ep: path where to export the binary
--extra-flags/-ef: extra flags to add to the command
--sudo/-S: specify if the exported item should be ran 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
;;
-s | --service)
if [ -n "$2" ]; then
export_action="service"
exported_service="$2"
shift
shift
fi
;;
-S | --sudo)
is_sudo="sudo"
shift
;;
-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
;;
*) # 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 ]; then
printf >&2 "You must run %s inside a container!\n" " $(basename "$0")"
exit 126
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}" ] &&
[ -z "${exported_service}" ]; 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 informations.\n"
exit 2
fi
# Ensure we're not receiving more than one action at time.
if [ -n "${exported_app}" ] && [ -n "${exported_bin}" ] ||
[ -n "${exported_app}" ] && [ -n "${exported_service}" ] ||
[ -n "${exported_bin}" ] && [ -n "${exported_service}" ]; 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
# We can assume this as we set it the same as container name during creation.
container_name=$(cat /etc/hostname)
# Prefix to add to an existing command to work throught the container
container_command_prefix="${DISTROBOX_ENTER_PATH:-"distrobox-enter"} -H -n ${container_name} -- \"${is_sudo} "
# 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 [ ! -f /run/.containerenv ]; then
${DISTROBOX_ENTER_PATH:-"distrobox-enter"} -n ${container_name} -- ${is_sudo} ${exported_bin} ${extra_flags} \$@
else
${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 ]; then
if [ ! -f "${dest_file}" ]; then
printf >&2 "Error: cannot find %s in %s.\nWas it exported?.\n" "$(basename "${exported_bin}")" "${dest_path}"
return 1
fi
if 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
else
printf >&2 "Error: %s exists but it's not a distrobox exported file.\n" "${dest_file}"
printf >&2 "Error: cannot delete: %s.\n" "${dest_file}"
return 2
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.
printf >&2 "A problem occurred.\n"
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/$HOME/.local/share/icons
# needed desktop files in /run/host/$HOME/.local/share/applications
# or error code.
export_application() {
# Ensure the app we're exporting is installed
if ! command -v "${exported_app}" >/dev/null; then
printf >&2 "Error: trying to export a non-installed application.\n"
return 127
fi
# Find desktop file for the application to export
desktop_files=$(grep -ril "${exported_app}" /usr/share/applications/* || :)
icon_files=$(find /usr/share/icons -iname "*${exported_app}*" || :)
# 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
# create applications dir if not existing
if [ ! -d "/run/host/${HOME}/.local/share/applications" ]; then
mkdir -p "/run/host/${HOME}/.local/share/applications"
fi
# copy icons in home directory
for icon_file in ${icon_files}; do
icon_home_directory="$(dirname "${icon_file}" | sed "s|/usr/share/|\/run\/host\/${HOME}/.local/share/|g")"
# check if we're exporting or deleting
if [ "${exported_delete}" -ne 0 ]; then
# we need to remove, not export
rm -f "${icon_home_directory}"/"$(basename "${icon_file}")"
else
# we wanto to export the application's icons
mkdir -p "${icon_home_directory}"
cp "${icon_file}" "${icon_home_directory}"
fi
done
# create desktop files for the distrobox
for desktop_file in ${desktop_files}; do
desktop_home_file="$(basename "${desktop_file}")"
# check if we're exporting or deleting
if [ "${exported_delete}" -ne 0 ]; then
if [ ! -f "/run/host/${HOME}/.local/share/applications/${desktop_home_file}" ]; then
printf >&2 "Error: trying to remove a non-exported application.\n"
return 1
fi
rm -f "/run/host/${HOME}/.local/share/applications/${desktop_home_file}"
else
# Add commmand_prefix
# Add extra flags
# Add closing quote
# If a TryExec is present, we have to fake it as it will not work
# throught the container separation
sed "s|^Exec=|Exec=${container_command_prefix} |g" "${desktop_file}" |
sed "s|\(%.*\)|${extra_flags} \1|g" |
sed "s|\(%.*\)|\" \1|g" |
sed "s|^TryExec=.*|TryExec=true|g" \
>"/run/host/${HOME}/.local/share/applications/${desktop_home_file}"
# in the end we add the final quote we've opened in the "container_command_prefix"
fi
done
printf "Application %s successfully exported.\nOK!\n" "${exported_app}"
printf "%s will appear in your applications list in a few seconds.\n" "${exported_app}"
}
# Export systemd service to the host.
# the following function will export a selected systemd unit from the distrobox
# to the host. It will modify the original unit to include the container_command_prefix.
#
# Arguments:
# none it will use the ones set up globally
# Outputs:
# new systemd unit in /run/host/$HOME/.config/systemd/user/
# or error code.
export_service() {
# find the service file in the common
service_file=$(find \
/etc/systemd/system/ /lib/systemd/system/ /usr/lib/systemd/system/ "${HOME}"/.config/systemd/user/ \
-type f -name "${exported_service}*" 2>/dev/null | tail -1)
# Check that we found some service files first.
if [ -z "${service_file}" ]; then
printf >&2 "Error: cannot find any service file for %s.\n" "${exported_service}"
printf >&2 "Error: trying to export a non-installed service.\n"
return 127
fi
# this is the output file we will produce.
exported_service_file="${container_name}-$(basename "${service_file}")"
exported_service_fullpath="/run/host/${HOME}/.config/systemd/user/${exported_service_file}"
# If we're deleting it, just do it and exit
if [ "${exported_delete}" -ne 0 ]; then
if [ ! -f "${exported_service_fullpath}" ]; then
printf >&2 "Error: cannot find service %s.\nWas it exported?.\n" "${exported_service_file}"
return 1
fi
rm -f "${exported_service_fullpath}"
printf "Service %s successfully removed.\nOK!\n" "${exported_service_file}"
return 0
fi
# Check if it is already exported
if [ -f "${exported_service_fullpath}" ] &&
grep -q "${container_command_prefix}" "${exported_service_fullpath}"; then
printf "Service %s is already exported.\n\n" "${exported_service_file}"
printf "\nTo check the status, run:\n\tsystemctl --user status %s \n" "${exported_service_file}"
printf "\nTo start it, run:\n\tsystemctl --user start %s \n" "${exported_service_file}"
printf "\nTo start it at login, run:\n\tsystemctl --user enable %s \n" "${exported_service_file}"
return 0
fi
# Create temp file with random name
temp_file="$(mktemp -u)"
# Replace all Exec occurrencies
if [ ! -d "/run/host/${HOME}/.config/systemd/user/" ]; then
mkdir -p "/run/host/${HOME}/.config/systemd/user/"
fi
cat "${service_file}" >"${exported_service_fullpath}" 2>/dev/null
for exec_cmd in ExecStart ExecStartPre ExecStartPost ExecReload ExecStop ExecStopPost; do
# Save to temp file
cat "${exported_service_fullpath}" >"${temp_file}" 2>/dev/null
# Add prefix only if not present
if ! grep "${exec_cmd}" "${temp_file}" | grep -q "${container_command_prefix}"; then
# Add commmand_prefix
# Add extra flags
# Add closing quote
sed "s|^${exec_cmd}=|${exec_cmd}=${container_command_prefix}|g" "${temp_file}" |
sed "s|^${exec_cmd}=.*|& ${extra_flags}|g" |
sed "s|^${exec_cmd}=.*|&\"|g" >"${exported_service_fullpath}"
# in the end we add the final quote we've opened in the "container_command_prefix"
fi
done
# Cleanup
rm -f "${temp_file}"
printf "Service %s successfully exported.\nOK\n" "${exported_service_file}"
printf "%s will appear in your services list in a few seconds.\n\n" "${exported_service_file}"
printf "\nTo check the status, run:\n\tsystemctl --user status %s \n" "${exported_service_file}"
printf "\nTo start it, run:\n\tsystemctl --user start %s \n" "${exported_service_file}"
printf "\nTo start it at login, run:\n\tsystemctl --user enable %s \n" "${exported_service_file}"
return 0
}
# Main routine
case "${export_action}" in
app)
export_application
;;
bin)
export_binary
;;
service)
export_service
;;
*)
printf >&2 "Invalid arguments, choose an action below.\n"
show_help
exit 2
;;
esac