#!/bin/bash YNH_APT_INSTALL_DEPENDENCIES_REPLACE="true" # Define and install dependencies with a equivs control file # # [packagingv1] # # This helper can/should only be called once per app # # example : ynh_install_app_dependencies dep1 dep2 "dep3|dep4|dep5" # # usage: ynh_install_app_dependencies dep [dep [...]] # | arg: dep - the package name to install in dependence. # | arg: "dep1|dep2|…" - You can specify alternatives. It will require to install (dep1 or dep2, etc). # # Requires YunoHost version 2.6.4 or higher. ynh_apt_install_dependencies() { # Add a comma for each space between packages. But not add a comma if the space separate a version specification. (See below) local dependencies="$(sed 's/\([^\<=\>]\)\ \([^(]\)/\1, \2/g' <<< "$@" | sed 's/|/ | /')" local version=$(ynh_read_manifest "version") local app_ynh_deps="${app//_/-}-ynh-deps" # Replace all '_' by '-', and append -ynh-deps # Handle specific versions if grep '[<=>]' <<< "$dependencies"; then # Replace version specifications by relationships syntax # https://www.debian.org/doc/debian-policy/ch-relationships.html # Sed clarification # [^(\<=\>] ignore if it begins by ( or < = >. To not apply twice. # [\<=\>] matches < = or > # \+ matches one or more occurence of the previous characters, for >= or >>. # [^,]\+ matches all characters except ',' # Ex: 'package>=1.0' will be replaced by 'package (>= 1.0)' dependencies="$(sed 's/\([^(\<=\>]\)\([\<=\>]\+\)\([^,]\+\)/\1 (\2 \3)/g' <<< "$dependencies")" fi # ############################## # # Specific tweaks related to PHP # # ############################## # # Check for specific php dependencies which requires sury # This grep will for example return "7.4" if dependencies is "foo bar php7.4-pwet php-gni" # The (?<=php) syntax corresponds to lookbehind ;) local specific_php_version=$(grep -oP '(?<=php)[0-9.]+(?=-|\>|)' <<< "$dependencies" | sort -u) if [[ -n "$specific_php_version" ]] then # Cover a small edge case where a packager could have specified "php7.4-pwet php5-gni" which is confusing [[ $(echo $specific_php_version | wc -l) -eq 1 ]] \ || ynh_die "Inconsistent php versions in dependencies ... found : $specific_php_version" dependencies+=", php${specific_php_version}, php${specific_php_version}-fpm, php${specific_php_version}-common" local old_php_version=$(ynh_app_setting_get --key=php_version) # If the PHP version changed, remove the old fpm conf if [ -n "$old_php_version" ] && [ "$old_php_version" != "$specific_php_version" ]; then if [[ -f "/etc/php/$php_version/fpm/pool.d/$app.conf" ]] then ynh_backup_if_checksum_is_different "/etc/php/$php_version/fpm/pool.d/$app.conf" ynh_remove_fpm_config fi fi # Store php_version into the config of this app ynh_app_setting_set --key=php_version --value=$specific_php_version # Set the default php version back as the default version for php-cli. if test -e /usr/bin/php$YNH_DEFAULT_PHP_VERSION then update-alternatives --set php /usr/bin/php$YNH_DEFAULT_PHP_VERSION fi elif grep --quiet 'php' <<< "$dependencies"; then ynh_app_setting_set --key=php_version --value=$YNH_DEFAULT_PHP_VERSION fi # Specific tweak related to Postgresql (cf end of the helper) local psql_installed="$(_ynh_apt_package_is_installed "postgresql-$PSQL_VERSION" && echo yes || echo no)" # The first time we run ynh_install_app_dependencies, we will replace the # entire control file (This is in particular meant to cover the case of # upgrade script where ynh_install_app_dependencies is called with this # expected effect) Otherwise, any subsequent call will add dependencies # to those already present in the equivs control file. if [[ $YNH_APT_INSTALL_DEPENDENCIES_REPLACE == "true" ]] then YNH_APT_INSTALL_DEPENDENCIES_REPLACE="false" else local current_dependencies="" if _ynh_apt_package_is_installed "${app_ynh_deps}" then current_dependencies="$(dpkg-query --show --showformat='${Depends}' ${app_ynh_deps}) " current_dependencies=${current_dependencies// | /|} fi dependencies="$current_dependencies, $dependencies" fi # ############################# # Actual install using equivs # # ############################# # Prepare the virtual-dependency control file for equivs local TMPDIR=$(mktemp --directory) cat >${TMPDIR}/control < ./equivs_log 2>&1 || { cat ./equivs_log; false; } LC_ALL=C dpkg --force-depends --install "./${app_ynh_deps}_${version}_all.deb" > ./dpkg_log 2>&1 ) # Then install the missing dependencies with apt install _ynh_apt_install --fix-broken || { # If the installation failed # (the following is ran inside { } to not start a subshell otherwise ynh_die wouldnt exit the original process) # Parse the list of problematic dependencies from dpkg's log ... # (relevant lines look like: "foo-ynh-deps depends on bar; however:") cat $TMPDIR/dpkg_log local problematic_dependencies="$(grep -oP '(?<=-ynh-deps depends on ).*(?=; however)' $TMPDIR/dpkg_log | tr '\n' ' ')" # Fake an install of those dependencies to see the errors # The sed command here is, Print only from 'Reading state info' to the end. [[ -n "$problematic_dependencies" ]] && _ynh_apt_install $problematic_dependencies --dry-run 2>&1 | sed --quiet '/Reading state info/,$p' | grep -v "fix-broken\|Reading state info" >&2 ynh_die "Unable to install dependencies" } rm --recursive --force "$TMPDIR" # Remove the temp dir. # check if the package is actually installed _ynh_apt_package_is_installed "${app_ynh_deps}" || ynh_die "Unable to install dependencies" # Specific tweak related to Postgresql # -> trigger postgresql regenconf if we may have just installed postgresql local psql_installed2="$(_ynh_apt_package_is_installed "postgresql-$PSQL_VERSION" && echo yes || echo no)" if [[ "$psql_installed" != "$psql_installed2" ]] then yunohost tools regen-conf postgresql fi } # Remove fake package and its dependencies # # [packagingv1] # # Dependencies will removed only if no other package need them. # # usage: ynh_apt_remove_dependencies # # Requires YunoHost version 2.6.4 or higher. ynh_apt_remove_dependencies() { local app_ynh_deps="${app//_/-}-ynh-deps" # Replace all '_' by '-', and append -ynh-deps local current_dependencies="" if _ynh_apt_package_is_installed "${app_ynh_deps}"; then current_dependencies="$(dpkg-query --show --showformat='${Depends}' ${app_ynh_deps}) " current_dependencies=${current_dependencies// | /|} fi # Edge case where the app dep may be on hold, # cf https://forum.yunohost.org/t/migration-error-cause-of-ffsync/20675/4 if apt-mark showhold | grep -q -w ${app_ynh_deps} then apt-mark unhold ${app_ynh_deps} fi # Remove the fake package and its dependencies if they not still used. # (except if dpkg doesn't know anything about the package, # which should be symptomatic of a failed install, and we don't want bash to report an error) if dpkg-query --show ${app_ynh_deps} &>/dev/null then _ynh_apt autoremove --purge ${app_ynh_deps} fi } # Install packages from an extra repository properly. # # [packagingv1] # # usage: ynh_apt_install_dependencies_from_extra_repository --repo="repo" --package="dep1 dep2" --key=key_url # | arg: -r, --repo= - Complete url of the extra repository. # | arg: -p, --package= - The packages to install from this extra repository # | arg: -k, --key= - url to get the public key. # # Requires YunoHost version 3.8.1 or higher. ynh_apt_install_dependencies_from_extra_repository() { # ============ Argument parsing ============= local -A args_array=([r]=repo= [p]=package= [k]=key=) local repo local package local key ynh_handle_getopts_args "$@" # =========================================== # Split the repository into uri, suite and components. repo="${repo#deb }" local uri="$(echo "$repo" | awk '{ print $1 }')" local suite="$(echo "$repo" | awk '{ print $2 }')" local component="${repo##$uri $suite }" # Add the new repo in sources.list.d mkdir --parents "/etc/apt/sources.list.d" echo "deb $uri $suite $component" > "/etc/apt/sources.list.d/$app.list" # Pin the new repo with the default priority, so it won't be used for upgrades. # Build $pin from the uri without http and any sub path local pin="${uri#*://}" pin="${pin%%/*}" # Pin repository mkdir --parents "/etc/apt/preferences.d" cat << EOF > "/etc/apt/preferences.d/$app" Package: * Pin: origin $pin Pin-Priority: 995 EOF mkdir --parents "/etc/apt/trusted.gpg.d" # Timeout option is here to enforce the timeout on dns query and tcp connect (c.f. man wget) wget --timeout 900 --quiet "$key" --output-document=- | gpg --dearmor | tee /etc/apt/trusted.gpg.d/$name.gpg >/dev/null # Update the list of package with the new repo NB: we use -o # Dir::Etc::sourcelist to only refresh this repo, because # ynh_apt_install_dependencies will also call an ynh_apt update on its own # and it's good to limit unecessary requests ... Here we mainly want to # validate that the url+key is correct before going further _ynh_apt update -o Dir::Etc::sourcelist="/etc/apt/sources.list.d/$app.list" # Install requested dependencies from this extra repository. # NB: because of the mechanism with $ynh_apt_install_DEPENDENCIES_REPLACE, # this will usually only *append* to the existing list of dependency, not # replace the existing $app-ynh-deps ynh_apt_install_dependencies "$package" # Force to upgrade to the last version... # Without doing apt install, an already installed dep is not upgraded local apps_auto_installed="$(apt-mark showauto $package)" _ynh_apt_install "$package" [ -z "$apps_auto_installed" ] || apt-mark auto $apps_auto_installed # Remove this extra repository after packages are installed ynh_safe_rm "/etc/apt/sources.list.d/$app.list" ynh_safe_rm "/etc/apt/preferences.d/$app" ynh_safe_rm "/etc/apt/trusted.gpg.d/$app.gpg" ynh_apt_update } ####################### # Internal misc utils # ####################### # Check if apt is free to use, or wait, until timeout. _ynh_wait_dpkg_free() { local try set +o xtrace # set +x # With seq 1 17, timeout will be almost 30 minutes for try in $(seq 1 17); do # Check if /var/lib/dpkg/lock is used by another process if lsof /var/lib/dpkg/lock >/dev/null; then echo "apt is already in use..." # Sleep an exponential time at each round sleep $((try * try)) else # Check if dpkg hasn't been interrupted and is fully available. # See this for more information: https://sources.debian.org/src/apt/1.4.9/apt-pkg/deb/debsystem.cc/#L141-L174 local dpkg_dir="/var/lib/dpkg/updates/" # For each file in $dpkg_dir while read dpkg_file <&9; do # Check if the name of this file contains only numbers. if echo "$dpkg_file" | grep --perl-regexp --quiet "^[[:digit:]]+$"; then # If so, that a remaining of dpkg. ynh_print_warn "dpkg was interrupted, you must manually run 'sudo dpkg --configure -a' to correct the problem." set -o xtrace # set -x return 1 fi done 9<<<"$(ls -1 $dpkg_dir)" set -o xtrace # set -x return 0 fi done echo "apt still used, but timeout reached !" set -o xtrace # set -x } # Check either a package is installed or not _ynh_apt_package_is_installed() { local package=$1 dpkg-query --show --showformat='${db:Status-Status}' "$package" 2>/dev/null \ | grep --quiet "^installed$" &>/dev/null } # Return the installed version of an apt package, if installed _ynh_apt_package_version() { if _ynh_apt_package_is_installed "$package"; then dpkg-query --show --showformat='${Version}' "$package" 2>/dev/null else echo '' fi } # APT wrapper for non-interactive operation _ynh_apt() { _ynh_wait_dpkg_free LC_ALL=C DEBIAN_FRONTEND=noninteractive apt-get --assume-yes --quiet -o=Acquire::Retries=3 -o=Dpkg::Use-Pty=0 $@ } # Wrapper around "apt install" with the appropriate options _ynh_apt_install() { _ynh_apt --no-remove --option Dpkg::Options::=--force-confdef \ --option Dpkg::Options::=--force-confold install $@ }