#!/bin/bash # check for packages that need to be built, and build a list in the proper build order # Details: # https://github.com/archlinux32/builder/wiki/Build-system#get-package-updates # TODOs: # be more secure in case of update while build(s) is/are still in progress # -> (stale) lock files, moving (or changing content of) loop lock files # dependencies declared inside a PKGBUILD's package function do not # correctly take into account CARCH (see package "gens" from community) . "${0%/*}/../conf/default.conf" usage() { >&2 echo '' >&2 echo 'get-package-updates: check for packages that need to be built,' >&2 echo ' and build a list in the proper build order' >&2 echo '' >&2 echo 'possible options:' >&2 echo ' -b|--block: If necessary, wait for lock blocking.' >&2 echo ' -h|--help: Show this help and exit.' >&2 echo ' -n|--no-pull: Do not pull git repos, merely reorder build list.' [ -z "$1" ] && exit 1 || exit $1 } eval set -- "$( getopt -o bhn \ --long block \ --long help \ --long no-pull \ -n "$(basename "$0")" -- "$@" || \ echo usage )" block_flag='-n' pull=true while true do case "$1" in -b|--block) block_flag='' ;; -h|--help) usage 0 ;; -n|--no-pull) pull=false ;; --) shift break ;; *) >&2 echo 'Whoops, forgot to implement option "'"$1"'" internally.' exit -1 ;; esac shift done if [ $# -ne 0 ]; then >&2 echo 'Too many arguments.' usage fi # delete_package package # mark $package for deletion delete_package() { echo "$1" >> \ "${work_dir}/deletion-list.new" sed -i "/^${1//./\\.} /d" "${work_dir}/build-list.new" } # Update git repositories (official packages, community packages and the repository of package customizations). for repo in "${repo_paths[@]}"; do # TODO: # this is somewhat redundant and slow -- improve it! git -C "${repo}" checkout -f master git -C "${repo}" clean -xdf git -C "${repo}" fetch if ${pull}; then git -C "${repo}" reset --hard origin/master fi done # Read previous git revision numbers from files. declare -A old_repo_revisions declare -A new_repo_revisions for repo in "${!repo_paths[@]}"; do old_repo_revisions["${repo}"]="$( cat "${work_dir}/${repo}.revision" 2> /dev/null || \ echo NONE )" new_repo_revisions["${repo}"]="$( git -C "${repo_paths["${repo}"]}" rev-parse HEAD | \ tee "${work_dir}/${repo}.revision.new" )" done # Create a lock file for build list. exec 9> "${build_list_lock_file}" if ! flock ${block_flag} 9; then >&2 echo 'come back (shortly) later - I cannot lock build list.' exit fi echo 'Check modified packages from the last update, and put them to the build list.' # Check modified packages from the last update, and put them to the build list. # If a package is updated, but already on the rebuild list, then just update the git revision number. # If a package is deleted, remove from the rebuild list, and add it to the deletion list. # If a new package is added, then ensure that it's not on the deletion list. cp "${work_dir}/build-list"{,.new} cp "${work_dir}/deletion-list"{,.new} for repo in "${!repo_paths[@]}"; do ( # if old revision unknown, mimic "git diff"-output if [ "${old_repo_revisions["${repo}"]}" == "NONE" ]; then git -C "${repo_paths["${repo}"]}" archive --format=tar HEAD | \ tar -t | \ sed 's|^|A\t|' else git -C "${repo_paths["${repo}"]}" diff --no-renames --name-status "${old_repo_revisions["${repo}"]}" HEAD fi ) | \ # only track changes in PKGBUILDs grep '/PKGBUILD$' | \ if [ "${repo}" == "archlinux32" ]; then # modify the directory structure from the modifiaction-repository # to the one of an original source repository sed 's|^\(.\t\)\([^/]\+\)/\([^/]\+\)/\(.\+\)$|\2 \1\3/repos/\2-x86_64/\4|' | \ while read -r pkg_repo rest; do echo "${new_repo_revisions["$(find_git_repository_to_package_repository "${pkg_repo}")"]} ${rest}" done else sed "s|^|${new_repo_revisions["${repo}"]} |" fi | \ grep '^\S\+ .\s[^/]\+/repos/[^/]\+/PKGBUILD$' | \ # ignore i686 grep -v -- '-i686/PKGBUILD$' | \ sed 's|^\(\S\+\) \(.\)\t\([^/]\+\)/repos/\([^/]\+\)-[^/-]\+/PKGBUILD$|\2 \3 \1 \4|' | \ # ignore staging and testing grep -v '\(staging\|testing\)$' | \ # ignore lib32- packages (they should all have a pendent built for x86_64) grep -v '^. lib32-' done | \ sort -u | \ while read -r mode package git_revision repository; do case "${mode}" in # new or modified PKGBUILD "A"|"M") sed -i "/^${package//./\\.} /d" "${work_dir}/build-list.new" echo "${package} ${git_revision} ${new_repo_revisions["archlinux32"]} ${repository}" >> \ "${work_dir}/build-list.new" sed -i "/^${package//./\\.}\$/d" "${work_dir}/deletion-list.new" ;; # deleted PKGBUILD "D") delete_package "${package}" ;; *) >&2 echo "unknown git diff mode '${mode}'" exit 1 ;; esac done sort -u "${work_dir}/deletion-list.new" > \ "${work_dir}/deletion-list.new.new" mv "${work_dir}/deletion-list.new"{.new,} echo 'Extract dependencies of packages.' # First, we extract the dependencies of each package. mkdir -p "${work_dir}/package-infos" while read -r package git_revision mod_git_revision repository; do file_prefix="${work_dir}/package-infos/${package}.${git_revision}.${mod_git_revision}" # extract infos from PKGBUILD if not existent yet if [ ! -e "${file_prefix}.builds" ] || \ [ ! -e "${file_prefix}.needs" ] || \ [ ! -e "${file_prefix}.packages" ]; then # delete cached values of old versions of this PKGBUILD ls -1 "${file_prefix%.*.*}."* 2> /dev/null | \ sed 's|^.*/||' | \ grep "^${package//./\\.}"'\.\([0-9a-f]\{40\}\.\)\{2\}\(builds\|needs\|packages\|SRCINFO\)$' | \ while read file; do rm "${work_dir}/package-infos/${file}" done PKGBUILD="$(find_pkgbuild "${package}" "${repository}")" if [ ! -r "${PKGBUILD}" ]; then echo "can't find PKGBUILD to package '${package}' from repository '${repository}': '${PKGBUILD}'" exit 1 fi ( cd "${PKGBUILD%/*}" apply_package_customizations mksrcinfo -o "${file_prefix}.SRCINFO" ) # otherwise this just calls for trouble sed -i '/=\s*$/d' "${file_prefix}.SRCINFO" # extract "builds" = provides \cup pkgname grep '^\('$'\t''provides\|pkgname\) = ' "${file_prefix}.SRCINFO" | \ cut -d= -f2 | \ sed 's|^\s\+||; s|[<>]$||' | \ sort -u > \ "${file_prefix}.builds" # extract "packages" = pkgname grep '^pkgname = ' "${file_prefix}.SRCINFO" | \ cut -d= -f2 | \ sed 's|^\s\+||; s|[<>]$||' | \ sort -u > \ "${file_prefix}.packages" # extract "needs" = ( makedepends \cup checkdepends ) \setminus "builds" ( ( # this would include runtime dependencies, too: # sed -n '/^pkgname = /q;/^'$'\t''depends = /p' "${file_prefix}.SRCINFO" grep '^'$'\t''\(makedepends\|checkdepends\) = ' "${file_prefix}.SRCINFO" ) | \ cut -d= -f2 | \ sed 's|^\s\+||; s|[<>]$||' | \ sort -u sed 'p' "${file_prefix}.builds" ) | \ sort | \ uniq -u > \ "${file_prefix}.needs" rm "${file_prefix}.SRCINFO" fi done < "${work_dir}/build-list.new" # ignore blacklisted packages and dependent packages # this is the first time when all the information is available and up to date black_listed='' black_listed_new="$( cat "${repo_paths["archlinux32"]}/blacklist" )" while [ -n "${black_listed_new}" ]; do black_listed="$( printf '%s\n%s' "${black_listed}" "${black_listed_new}" )" black_listed_new="$( echo "${black_listed_new}" | \ while read -r bl_package; do echo "${bl_package}" echo "${bl_package}" ls "${work_dir}/package-infos/" | \ grep '\.needs$' | \ while read -r package_info; do grep -q "^${bl_package//./\\.}\$" "${work_dir}/package-infos/${package_info}" && \ echo "${package_info%.*.*.needs}" done done | \ sort | \ uniq -u )" done echo "${black_listed}" | \ while read -r package; do [ -n "${package}" ] && \ delete_package "${package}" done # Now we create the partial order. while read -r package git_revision mod_git_revision repository; do # add "$pkgname -> $build-target" to build-order list sed "s|^|${package} |" "${work_dir}/package-infos/${package}.${git_revision}.${mod_git_revision}.builds" # add "$dependency -> $pkgname" to build-order list sed "s|\$| ${package}|" "${work_dir}/package-infos/${package}.${git_revision}.${mod_git_revision}.needs" done \ < "${work_dir}/build-list.new" \ > "${work_dir}/build-order" echo 'Now actually sort it.' ( # this part will have the correct build order, but all the infos are missing tsort "${work_dir}/build-order" 2> "${work_dir}/tsort.error" | \ nl -ba | \ awk '{print $1 " not-git also-not-git whatever " $2}' # this part has all the infos, but possibly the wrong order awk '{print "0 " $2 " " $3 " " $4 " " $1}' "${work_dir}/build-list.new" ) | \ sort -k5,5 -k1nr | \ # now, we have the correct order and the infos, but in adjacent lines uniq -f4 -D | \ sed '/^0 /d;N;s|\n| |' | \ # now in one line, each sort -k1n,1 | \ awk '{print $5 " " $7 " " $8 " " $9}' > \ "${work_dir}/build-list.new.new" rm --one-file-system -rf "${work_dir}/build-list.loops.new" mkdir "${work_dir}/build-list.loops.new" if [ -s "${work_dir}/tsort.error" ]; then >&2 echo 'WARNING: There is a dependency cycle!' >&2 cat "${work_dir}/tsort.error" >&2 echo >&2 echo 'I will continue anyway.' # save loops in separate files each, so breaking them is easier awk ' /^tsort: \S+: input contains a loop:$/{ n++; getline } { print $2 >"'"${work_dir}"'/build-list.loops.new/loop_" n } ' "${work_dir}/tsort.error" # remove lines from loop files which are no packages ls "${work_dir}/build-list.loops.new" | \ grep '^loop_[0-9]\+$' | \ while read -r loop; do ( sort -u "${work_dir}/build-list.loops.new/${loop}" cut -d' ' -f1 "${work_dir}/build-list.new.new" | \ sort -u ) | \ sort | \ uniq -d > \ "${work_dir}/build-list.loops.new/${loop}.new" mv "${work_dir}/build-list.loops.new/${loop}"{.new,} done else rm "${work_dir}/tsort.error" fi # Move the .new-files to the actual files rm -rf --one-file-system "${work_dir}/build-list.loops" ( echo "build-list.loops" "build-list.new" "build-list" "deletion-list" echo "${!repo_paths[@]}" | \ sed 's@\( \|$\)@.revision\1@g' ) | \ tr ' ' '\n' | \ while read -r file; do mv "${work_dir}/${file}.new" "${work_dir}/${file}" done # Remove the lock file rm -f "${build_list_lock_file}"