#!/usr/bin/env bash # # Copyright (c) 2024 YunoHost Contributors # # This file is part of YunoHost (see https://yunohost.org) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program 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 Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # YNH_APT_INSTALL_DEPENDENCIES_REPLACE="true" # Define and install dependencies with a equivs control file # # example : ynh_apt_install_dependencies dep1 dep2 "dep3|dep4|dep5" # # usage: ynh_apt_install_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). # 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_config_remove_phpfpm 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 # Otherwise force php_version back to the default version ... # ... except we don't want to do this when appending the "extra" dependencies if they don't contain a php version ... elif grep --quiet 'php' <<< "$dependencies" && [[ $YNH_APT_INSTALL_DEPENDENCIES_REPLACE == "true" ]]; 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_apt_install_dependencies, we will replace the # entire control file (This is in particular meant to cover the case of # upgrade script where ynh_apt_install_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 # # ################ # Prepare the virtual-dependency control file for dpkg-deb --build local TMPDIR=$(mktemp --directory) mkdir -p "${TMPDIR}/${app_ynh_deps}/DEBIAN" # For some reason, dpkg-deb insists for folder perm to be 755 and sometimes it's 777 o_O? chmod -R 755 "${TMPDIR}/${app_ynh_deps}" cat > "${TMPDIR}/${app_ynh_deps}/DEBIAN/control" << EOF Section: misc Priority: optional Package: ${app_ynh_deps} Version: ${version} Depends: ${dependencies//,,/,} Architecture: all Maintainer: root@localhost Description: Fake package for ${app} (YunoHost app) dependencies This meta-package is only responsible of installing its dependencies. EOF _ynh_apt update --error-on=any _ynh_wait_dpkg_free # Install the fake package without its dependencies with dpkg --force-depends if ! LC_ALL=C dpkg-deb --build "${TMPDIR}/${app_ynh_deps}" "${TMPDIR}/${app_ynh_deps}.deb" > "${TMPDIR}/dpkg_log" 2>&1; then cat "${TMPDIR}/dpkg_log" >&2 ynh_die "Unable to install dependencies" fi # Don't crash in case of error, because is nicely covered by the following line LC_ALL=C dpkg --force-depends --install "${TMPDIR}/${app_ynh_deps}.deb" 2>&1 | tee "${TMPDIR}/dpkg_log" || true # 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 mapfile -t problematic_dependencies < <(grep -oP '(?<=-ynh-deps depends on ).*(?=; however)' "$TMPDIR/dpkg_log") # Fake an install of those dependencies to see the errors # The sed command here is, Print only from 'Reading state info' to the end. if ((${#problematic_dependencies[@]} != 0)); then _ynh_apt_install "${problematic_dependencies[@]}" --dry-run 2>&1 | sed --quiet '/Reading state info/,$p' | grep -v "fix-broken\|Reading state info" >&2 fi ynh_die "Unable to install apt dependencies, it might be due to a conflict with another app - or you should check and share the previous log about what are the problematic 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 apt 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 # # Dependencies will removed only if no other package need them. # # usage: ynh_apt_remove_dependencies 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. # # usage: ynh_apt_install_dependencies_from_extra_repository --repo="repo" --package="dep1 dep2" --key=key_url # | arg: --repo= - Complete url of the extra repository. # | arg: --package= - The packages to install from this extra repository # | arg: --key= - url to get the public key. # 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 package into packages list local packages read -r -a packages <<< "$package" # Split the repository into uri, suite and components. IFS=', ' read -r -a repo_parts <<< "$repo" index=0 # Remove "deb " at the beginning of the repo. if [[ "${repo_parts[0]}" == "deb" ]]; then index=1 fi uri="${repo_parts[$index]}" index=$((index + 1)) suite="${repo_parts[$index]}" index=$((index + 1)) # Get the components if (("${#repo_parts[@]}" > 0)); then component="${repo_parts[*]:$index}" fi if [[ "$key" == "trusted=yes" ]]; then trust="[trusted=yes]" else trust="" fi # Add the new repo in sources.list.d mkdir --parents "/etc/apt/sources.list.d" echo "deb $trust $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 if [ -n "$key" ] && [[ "$key" != "trusted=yes" ]]; then 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 > "/etc/apt/trusted.gpg.d/$app.gpg" fi # 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 --error-on=any -o Dir::Etc::sourcelist="/etc/apt/sources.list.d/$app.list" # Force the cache to be reupdated on the next "apt update" (in # ynh_apt_install_dependencies) because the previous command with # ::sourcelist option makes apt forget about every other package for other, # so we want to force the cache to be reupdated entirely touch "/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 mapfile -t apps_auto_installed < <(apt-mark showauto "${packages[@]}") _ynh_apt_install "${packages[@]}" if ((${#apps_auto_installed[@]} != 0)); then apt-mark auto "${apps_auto_installed[@]}" fi # 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 --error-on=any } # ##################### # 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 -r 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() { # Optimization when just calling apt update : check if the cache was # already refreshed in the last 30 min, which should be enough and prevent # unecessary traffic and annoying wait time during app dependency installs etc if [[ "$*" == "update" ]]; then # trick from https://stackoverflow.com/a/205710 local aptcache="/var/cache/apt/pkgcache.bin" sleep 1 if [[ -e $aptcache ]] && [[ -n "$(find $aptcache -mmin -30)" ]] && [[ -z "$(find /etc/apt/ -newer $aptcache)" ]]; then echo "apt cache was already updated in the last 30 minutes, skipping 'apt update'" return fi fi _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 "$@" }