#!/usr/bin/env bash set -u # Following variables should be set in environment outside of this script. # Build updated packages. : "${BUILD_PACKAGES:=false}" # Commit changes to Git. : "${GIT_COMMIT_PACKAGES:=false}" # Push changes to remote. : "${GIT_PUSH_PACKAGES:=false}" export TERMUX_PKG_UPDATE_METHOD="" # Which method to use for updating? (repology, github or gitlab) export TERMUX_PKG_UPDATE_TAG_TYPE="" # Whether to use latest-release-tag or newest-tag. export TERMUX_GITLAB_API_HOST="gitlab.com" # Default host for gitlab-ci. export TERMUX_PKG_AUTO_UPDATE=false # Whether to auto-update or not. Disabled by default. export TERMUX_PKG_UPDATE_VERSION_REGEXP="" # Regexp to extract version with `grep -oP`. export TERMUX_PKG_UPDATE_VERSION_SED_REGEXP="" # Regexp to extract version with `sed`. export TERMUX_REPOLOGY_DATA_FILE TERMUX_REPOLOGY_DATA_FILE="$(mktemp -t termux-repology.XXXXXX)" # File to store repology data. export TERMUX_SCRIPTDIR TERMUX_SCRIPTDIR=$(realpath "$(dirname "$0")/../..") # Root of repository. export TERMUX_PACKAGES_DIRECTORIES TERMUX_PACKAGES_DIRECTORIES=$(jq --raw-output 'del(.pkg_format) | keys | .[]' "${TERMUX_SCRIPTDIR}"/repo.json) # Define few more variables used by scripts. # shellcheck source=scripts/properties.sh . "${TERMUX_SCRIPTDIR}/scripts/properties.sh" # Utility function to write error message to stderr. # shellcheck source=scripts/updates/utils/termux_error_exit.sh . "${TERMUX_SCRIPTDIR}"/scripts/updates/utils/termux_error_exit.sh # Utility function to write updated version to build.sh. # shellcheck source=scripts/updates/utils/termux_pkg_upgrade_version.sh . "${TERMUX_SCRIPTDIR}"/scripts/updates/utils/termux_pkg_upgrade_version.sh # Utility function to check if package needs to be updated, based on version comparison. # shellcheck source=scripts/updates/utils/termux_pkg_is_update_needed.sh . "${TERMUX_SCRIPTDIR}"/scripts/updates/utils/termux_pkg_is_update_needed.sh # Wrapper around github api to get latest release or newest tag. # shellcheck source=scripts/updates/api/termux_github_api_get_tag.sh . "${TERMUX_SCRIPTDIR}"/scripts/updates/api/termux_github_api_get_tag.sh # Wrapper around gitlab api to get latest release or newest tag. # shellcheck source=scripts/updates/api/termux_gitlab_api_get_tag.sh . "${TERMUX_SCRIPTDIR}"/scripts/updates/api/termux_gitlab_api_get_tag.sh # Function to get latest version of a package as per repology. # shellcheck source=scripts/updates/api/termux_repology_api_get_latest_version.sh . "${TERMUX_SCRIPTDIR}"/scripts/updates/api/termux_repology_api_get_latest_version.sh # Default auto update script for packages hosted on github.com. Should not be overrided by build.sh. # To use custom algorithm, one should override termux_pkg_auto_update(). # shellcheck source=scripts/updates/internal/termux_github_auto_update.sh . "${TERMUX_SCRIPTDIR}"/scripts/updates/internal/termux_github_auto_update.sh # Default auto update script for packages hosted on hosts using gitlab-ci. Should not be overrided by build.sh. # To use custom algorithm, one should override termux_pkg_auto_update(). # shellcheck source=scripts/updates/internal/termux_gitlab_auto_update.sh . "${TERMUX_SCRIPTDIR}"/scripts/updates/internal/termux_gitlab_auto_update.sh # Default auto update script for rest packages. Should not be overrided by build.sh. # To use custom algorithm, one should override termux_pkg_auto_update(). # shellcheck source=scripts/updates/internal/termux_repology_auto_update.sh . "${TERMUX_SCRIPTDIR}"/scripts/updates/internal/termux_repology_auto_update.sh # Main script to: # - by default, decide which update method to use, # - but can be overrided by build.sh to use custom update method. # - For example: see neovim-nightly's build.sh. # shellcheck source=scripts/updates/termux_pkg_auto_update.sh . "${TERMUX_SCRIPTDIR}"/scripts/updates/termux_pkg_auto_update.sh # Converts milliseconds to human-readable format. # Example: `ms_to_human_readable 123456789` => 34h 17m 36s 789ms ms_to_human_readable() { echo "$(($1/3600000))h $(($1%3600000/60000))m $(($1%60000/1000))s $(($1%1000))ms" | sed 's/0h //;s/0m //;s/0s //' } # Runs a command without displaying its output in the case if trace is disabled and displays output in the case if it is enabled. # Needed only for debugging quiet() { if [[ "$-" =~ x ]]; then "$@" else &>/dev/null "$@" fi return $? } _update() { export TERMUX_PKG_NAME TERMUX_PKG_NAME="$(basename "$1")" export TERMUX_PKG_BUILDER_DIR TERMUX_PKG_BUILDER_DIR="$(realpath "$1")" # Directory containing build.sh. IFS="," read -r -a BLACKLISTED_ARCH <<<"${TERMUX_PKG_BLACKLISTED_ARCHES:-}" export TERMUX_ARCH="" # Arch to test updates. for arch in aarch64 arm i686 x86_64; do # shellcheck disable=SC2076 if [[ ! " ${BLACKLISTED_ARCH[*]} " =~ " ${arch} " ]]; then TERMUX_ARCH="${arch}" break fi done # Set +e +u to avoid: # - ending on errors such as $(which prog), where prog is not installed. # - error on unbound variable. (All global variables used should be covered by properties.sh and above.) set +e +u # shellcheck source=/dev/null . "${TERMUX_PKG_BUILDER_DIR}"/build.sh 2>/dev/null set -e -u echo # Newline. echo "INFO: Updating ${TERMUX_PKG_NAME} [Current version: ${TERMUX_PKG_VERSION}]" termux_pkg_auto_update } declare -A _LATEST_TAGS=() declare -A _FAILED_UPDATES=() declare -a _ALREADY_SEEN=() # Array of packages successfully updated or skipped. # _fetch_and_cache_tags fetches all possible tags using termux_pkg_auto_update, but using Ninja build system. # The key difference is that we make the process concurrent, allowing us to fetch tags simultaneously rather than one at a time. # Once all tags are cached, the termux_pkg_auto_update function will operate much more quickly. # We avoid packages with overwritten termux_pkg_auto_update to prevent unexpected modifications to the package`s build.sh. _fetch_and_cache_tags() { if [ "$(uname -o)" = "Android" ] || [ -e "/system/bin/app_process" ]; then if ! command -v ninja &> /dev/null; then echo "INFO: Skipping fetching and caching tags. Package 'ninja' is not installed." return 0 fi fi if ! command -v ninja &> /dev/null; then echo "INFO: Fetching ninja build system" . "${TERMUX_SCRIPTDIR}"/scripts/build/termux_download.sh . "${TERMUX_SCRIPTDIR}"/scripts/build/setup/termux_setup_ninja.sh TERMUX_ON_DEVICE_BUILD=false TERMUX_COMMON_CACHEDIR=/tmp TERMUX_PKG_TMPDIR=/tmp termux_setup_ninja fi echo "INFO: Fetching and caching tags" # First invocation of termux_repology_api_get_latest_version fetches and caches repology metadata. quiet termux_repology_api_get_latest_version ' ' local __PACKAGES=() for repo_dir in $(jq --raw-output 'del(.pkg_format) | keys | .[]' "${TERMUX_SCRIPTDIR}/repo.json"); do for pkg_dir in "${repo_dir}"/*; do ! quiet _should_update "${pkg_dir}" && continue # Skip if not needed. grep -q '^termux_pkg_auto_update' "${pkg_dir}/build.sh" && continue # Skip if package has custom auto-update __PACKAGES+=("${pkg_dir}") done done __main__() { cd ${TERMUX_SCRIPTDIR} export TERMUX_PKG_NAME="${1##*/}" TERMUX_PKG_BUILDER_DIR=${1} set +eu; for i in scripts/updates/{**/,}*.sh "${1}/build.sh"; do source ${i}; done set -eu termux_pkg_upgrade_version() { [[ -n "$1" ]] && echo "PKG|$TERMUX_PKG_NAME|${1#*:}" exit 0 } termux_repology_auto_update() { echo "PKG|$TERMUX_PKG_NAME|$(termux_repology_api_get_latest_version "${TERMUX_PKG_NAME}" | sed "s/^null$/${TERMUX_PKG_VERSION#*:}/")" exit 0 } termux_pkg_auto_update } __generate__() { echo "rule update" echo " command = bash -c \"\$\$F\" -- \$pkg_dir ||:" sed 's/[^ ]\+/\nbuild &: update\n pkg_dir=&/g' <<< "${__PACKAGES[@]}" echo "build run_all: phony ${__PACKAGES[@]}" echo "default run_all" } local LATEST_TAGS="$(\ F="$(declare -p TERMUX_SCRIPTDIR GITHUB_TOKEN TERMUX_REPOLOGY_DATA_FILE); $(declare -f __main__ | sed 's/__main__ ()//')" \ env --chdir=/tmp ninja -f /dev/stdin <<< "$(__generate__)" |& grep "^PKG|" )" unset -f __main__ __generate__ while IFS='|' read -r _ pkg version; do _LATEST_TAGS["${pkg:-_}"]="$version" done <<< "$LATEST_TAGS" quiet declare -p _LATEST_TAGS } _check_updated() { if [[ -n "${_LATEST_TAGS[${1##*/}]:-}" ]]; then ( set +eu quiet source "${1}/build.sh" set -eu export TERMUX_PKG_UPGRADE_VERSION_DRY_RUN=1 if quiet termux_pkg_upgrade_version "${_LATEST_TAGS[${1##*/}]}"; then echo "INFO: Skipping ${1##*/}: already at version ${TERMUX_PKG_VERSION#*:}" return 0 fi return 1 ) local _ANSWER=$? if (( _ANSWER == 0 )) ; then _ALREADY_SEEN+=("$(basename "${1}")") fi return $_ANSWER fi return 1 } _run_update() { local pkg_dir="$1" [[ -n "${_LATEST_TAGS[${pkg_dir##*/}]:-}" ]] && export __CACHED_TAG="${_LATEST_TAGS[${pkg_dir##*/}]}" # Run each package update in separate process since we include their environment variables. local output="" { output=$( set -euo pipefail _update "${pkg_dir}" 2>&1 | tee /dev/fd/3 # fd 3 is used to output to stdout as well. exit "${PIPESTATUS[0]}" # Return exit code of _update. ) } 3>&1 # shellcheck disable=SC2181 if [[ $? -ne 0 ]]; then _FAILED_UPDATES["$(basename "${pkg_dir}")"]="${output}" else _ALREADY_SEEN+=("$(basename "${pkg_dir}")") fi unset __CACHED_TAG } declare -a _CACHED_ISSUE_TITLES=() # Check if an issue with same title already exists and is open. _gh_check_issue_exists() { local pkg_name="$1" if [[ -z "${_CACHED_ISSUE_TITLES[*]}" ]]; then while read -r title; do _CACHED_ISSUE_TITLES+=("'${title}'") # An extra quote ('') is added to avoid false positive matches. done <<<"$( gh issue list \ --label "auto update failing" --label "bot" \ --state open \ --search "Auto update failing for in:title type:issue" \ --json title | jq -r '.[] | .title' )" fi # shellcheck disable=SC2076 # We want literal match here, not regex based. if [[ "${_CACHED_ISSUE_TITLES[*]}" =~ "'Auto update failing for ${pkg_name}'" ]]; then return 0 fi return 1 } _should_update() { local pkg_dir="$1" if [[ ! -f "${pkg_dir}/build.sh" ]]; then # Fail if detected a non-package directory. termux_error_exit "ERROR: directory '${pkg_dir}' is not a package." fi if { [[ "${PACKAGES_OPT_OUT:-}" == "true" ]] && grep -q '^TERMUX_PKG_AUTO_UPDATE=false$' "${pkg_dir}/build.sh"; } || \ { [[ "${PACKAGES_OPT_OUT:-}" != "true" ]] && ! grep -q '^TERMUX_PKG_AUTO_UPDATE=true$' "${pkg_dir}/build.sh"; }; then return 1 # Skip. fi # shellcheck disable=SC2076 if [[ " ${_ALREADY_SEEN[*]} ${!_FAILED_UPDATES[*]} " =~ " $(basename "${pkg_dir}") " ]]; then return 1 # Skip. fi if [[ "${GITHUB_ACTIONS:-}" == "true" ]] && _gh_check_issue_exists "$(basename "${pkg_dir}")"; then echo "INFO: Skipping '$(basename "${pkg_dir}")', an update issue for it hasn't been resolved yet." return 1 fi return 0 } shopt -s extglob _update_dependencies() { local pkg_dir="$1" if ! grep -qE "^(TERMUX_PKG_DEPENDS|TERMUX_PKG_BUILD_DEPENDS|TERMUX_SUBPKG_DEPENDS)=" \ "${pkg_dir}"/+(build|*.subpackage).sh; then return 0 fi # shellcheck disable=SC2086 # Allow splitting of TERMUX_PACKAGES_DIRECTORIES. while read -r dep dep_dir; do if [[ -z $dep ]]; then continue elif [[ "${dep}" == "ERROR" ]]; then termux_error_exit "ERROR: Obtaining update order failed for $(basename "${pkg_dir}")" fi _should_update "${dep_dir}" && ! _check_updated "${dep_dir}" && _run_update "${dep_dir}" done <<<"$("${TERMUX_SCRIPTDIR}"/scripts/buildorder.py "${pkg_dir}" $TERMUX_PACKAGES_DIRECTORIES || echo "ERROR")" } echo "INFO: Running update for: $*" if [[ "$1" == "@all" ]]; then _fetch_and_cache_tags for repo_dir in $(jq --raw-output 'del(.pkg_format) | keys | .[]' "${TERMUX_SCRIPTDIR}/repo.json"); do for pkg_dir in "${repo_dir}"/*; do _unix_millis=$(date +%s%N | cut -b1-13) ! _should_update "${pkg_dir}" && continue # Skip if not needed. _check_updated "${pkg_dir}" && continue # Skip if already up-to-date. # Update all its dependencies first. _update_dependencies "${pkg_dir}" # NOTE: I am not cheacking whether dependencies were updated successfully or not. # There is no way I could know whether this package will build with current # available verions of its dependencies or needs new ones. # So, whatever the case may be. We just need packages to be updated in order # and not care about anything else in between. If something fails to update, # it will be reported by failure handling code, so no worries. _run_update "${pkg_dir}" echo "termux - took $(ms_to_human_readable $(( $(date +%s%N | cut -b1-13) - _unix_millis )))" done done else for pkg in "$@"; do if [ ! -d "${pkg}" ]; then # If only package name is given, try to find it's directory. for repo_dir in $(jq --raw-output 'del(.pkg_format) | keys | .[]' "${TERMUX_SCRIPTDIR}/repo.json"); do if [ -d "${repo_dir}/${pkg}" ]; then pkg="${repo_dir}/${pkg}" break fi done fi # Here `pkg` is a directory. ! _should_update "${pkg}" && continue _update_dependencies "${pkg}" _run_update "${pkg}" done fi ################################################FAILURE HANDLING################################################# _gh_create_new_issue() { local pkg_name="$1" local max_body_length=65536 # Max length of the body for one request. local assignee="${GITHUB_ACTOR:-}" local issue_number local body local link if [[ "${assignee:-termuxbot2}" == "termuxbot2" ]]; then assignee="MrAdityaAlok" # Assign myself if termuxbot2 is the actor. fi # Extract origin URL, commit hash and builder directory and put everything together link="$(git config --get remote.origin.url |sed -E 's|\.git$||; s|git@([^:]+):(.+)|https://\1/\2|')/blob/$(git rev-parse HEAD)/$(echo */$1)" body="$( cat <<-EOF Hi, I'm Termux 🤖. I'm here to help you update your Termux packages. I've tried to update the [${pkg_name}](${link}) package, but it failed. Here's the output of the update script:
Show log
${_FAILED_UPDATES["${pkg_name}"]}

Above error occured when I last tried to update at $(date -u +"%Y-%m-%d %H:%M:%S UTC").
Run ID: ${GITHUB_RUN_ID}

Note: Automatic updates will be disabled until this issue is resolved.
EOF )" issue_number=$( gh issue create \ --title "Auto update failing for ${pkg_name}" \ --body "${body:0:${max_body_length}}" \ --label "auto update failing" --label "bot" \ --assignee ${assignee} | grep -oE "[0-9]+" # Last component of the URL returned is the issue number. ) if [ -z "${issue_number}" ]; then echo "ERROR: Failed to create issue." return 1 fi echo "INFO: Created issue ${issue_number} for ${pkg_name}." if [[ -n "${body:${max_body_length}}" ]]; then # The body was too long, so we need to append the rest. while true; do body="${body:${max_body_length}}" if [[ -z "${body}" ]]; then break fi sleep 5 # Otherwise we might get rate limited. gh issue edit "$issue_number" \ --body-file - <<<"$( gh issue view "$issue_number" \ --json body \ --jq '.body' )${body:0:${max_body_length}}" >/dev/null # NOTE: we use --body-file instead of --body to avoid shell error 'argument list too long'. done fi } _handle_failure() { echo # Newline. if [[ "${GITHUB_ACTIONS:-}" == "true" ]]; then echo "INFO: Creating issue for failed updates...(if any)" for pkg_name in "${!_FAILED_UPDATES[@]}"; do _gh_create_new_issue "${pkg_name}" done else echo "==> Failed updates:" local count=0 for pkg_name in "${!_FAILED_UPDATES[@]}"; do count=$((count + 1)) echo "${count}. ${pkg_name}" done exit 1 fi } if [[ ${#_FAILED_UPDATES[@]} -gt 0 ]]; then _handle_failure fi