#!/bin/sh -e HERE="$(dirname $(readlink -f "${0}"))"; TEMP="$(mktemp -d)"; cd "${HERE}"/..; # repository root (do not touch!) ## # README # # This script will determine which package(s) need to be # built or rebuilt by evaluating the state of mirrormaster # (or "next.adelielinux.org" in the case of a staging box) # and comparing it to the state of an official builder. # # It is to be run only on an official builder by someone # with access to and training to operate such a machine, # which may include automation and robots. # # This script assumes it runs inside an autobuilder 'debug' # environment. This is not an optional requirement. This is # an internal tool not intended for public-facing usage. # # This script assumes "next" ('NEXT') and "ours" ('OURS') are # synchronized. That is, binaries at the two locations are # either the same, or point to the same location. This is to # warn the operator that mirrormaster might be out of sync # with builders, and is not useful for developers. ## # TODO # # When checking 'DESCRIPTION' from APKINDEX, this is NOT # a reliable way to see what is *actually* the last package # built, and from what commit hash. This will cause issues! # # The computed list is too broad, specifically the 'comm -23' # line on 'list.*.sort' files. # # Register trap handlers to clean up and restore backups # appropriately; biggest risk is inconsistent state. Second # thing to avoid is wasted CPU time or having to restart. # # Implement build logs as in normal autobuilder. # # Performance enhancements. # # Mathematically rigorous series of transformations to # compute various bits here, not just a series of loops, # lists, etc. # ## # Configuration (use defaults for production!). # NEXT=https://next.adelielinux.org; OURS=/packages; REPO="system user"; # public-facing repositories ARCH="${AB_ARCH}"; # from autobuilder environment BNCH=current; # probably do not change this ## # Helpers. # message () { printf " * %s...\n" "${1}"; } fetch_local () { cat "${1}"; } fetch_curly () { curl -sL "${1}"; } find_strategy () { case "${1}" in /*) down=fetch_local; ;; # local filesystem *://*) down=fetch_curly; ;; # remote resource *) printf "E: unsupported site type!\n" && exit 1; ;; esac printf "%s\n" "${down}"; } ## # Counts number of commits between two commits (A,B] # # LAST: latest commit available (e.g. origin/current) # FROM: commit to compare # count_behind () { f_last="${1}"; shift; f_from="${1}"; shift; git log --format="%H" ${f_from}..${f_last} | wc -l; } ## # Manually update (if greater than global) to work around # a shell limitation in updating globals from subshells. # # NOTE: USES GLOBAL. # count_update () { f_this="${1}"; shift; # store the newest maximum if test "${OLD_this}" -lt "${f_this}"; then OLD_this=${f_this}; fi if test "${OLD_keep}" -lt "${f_this}"; then OLD_keep=${f_this}; fi } ## # String comparison, but with more screaming. # check_match () { f_next="${1}"; shift; f_ours="${1}"; shift; f_repo="${1}"; shift; # OK if ours is newer than next hind=$(count_behind "${f_next}" "${f_ours}"); test "${hind}" -eq 0 && return; printf "E: "next" ('%s') != "ours" ('%s') in repo '%s'!\n" "${f_next}" "${f_ours}" "${f_repo}"; exit 1; } ## # Produce a sorted list of files in a given ON-DISK # binary repository. TODO: update requirements so that # the "ours" repository is always required to be on-disk. # list_packages () { f_repo="${1}"; shift; find "${OURS}"/${f_repo}/${ARCH} | sort; } ## # Sanity checks. # message "Verifying configuration and environment"; test -z ${ARCH} && printf "E: 'ARCH' could not be determined!\n" && exit 1; command -v curl >/dev/null 2>&1 || (printf "E: required utility 'curl' not found!\n" && exit 1); ## # Internal. # message "Identifying transfer strategies"; _NEXT=$(find_strategy "${NEXT}"); _OURS=$(find_strategy "${OURS}"); ## # Update source code, if needed. # message "Updating local source repository" git checkout "${BNCH}"; git pull --ff-only; ## # Do it. # # Note: underscore-prefixed variables refer to friendly names. # Sometimes these variables need to come before, or after, due # to whether we use the friendly name to calculate commit, or # the other way around. This is intentional and ugly. Sorry. # print_status () { f_code="${1}"; shift; f_kind="${1}"; shift; f_repo="${1}"; shift; f_hash="${1}"; shift; f_many="${1}"; shift; f_name="${1}"; shift; printf " - [%s] %s (%s)\tis at %s [-%4d] (%s)...\n" \ "${f_code}" \ "${f_kind}" \ "${f_repo}" \ "${f_hash}" \ "${f_many}" \ "${f_name}" \ ; } message "Examining repositories"; meta=$(git log --format="%H" . | head -n 1); _meta=$(git describe ${meta}); nmeta=$(count_behind "${meta}" "${meta}"); # redundant i know, but consistent :D print_status REF .git meta "${meta}" "${nmeta}" "${_meta}"; # source of truth OLD_list=""; # starts empty, CURRENTLY UNUSED but probably useful! OLD_keep=0; # global maximum for repo in ${REPO}; do OLD_this=0; # reset zero at each iteration # disk disk=$(git log --format="%H" ${repo} | head -n 1); # last commit in repository on disk _disk=$(git describe "${disk}"); ndisk=$(count_behind "${meta}" "${disk}"); count_update "${ndisk}"; print_status SRC .git "${repo}" "${disk}" "${ndisk}" "${_disk}"; # next ${_NEXT} ${NEXT}/${repo}/${ARCH}/APKINDEX.tar.gz | tar -C "${TEMP}" -xzf - DESCRIPTION; _next=$(cut -d\ -f2 < "${TEMP}"/DESCRIPTION); next=$(git log --format="%H" "${_next}" | head -n 1); nnext=$(count_behind "${meta}" "${next}"); count_update "${nnext}"; # ours ${_OURS} ${OURS}/${repo}/${ARCH}/APKINDEX.tar.gz | tar -C "${TEMP}" -xzf - DESCRIPTION; _ours=$(cut -d\ -f2 < "${TEMP}"/DESCRIPTION); ours=$(git log --format="%H" "${_ours}" | head -n 1); nours=$(count_behind "${meta}" "${ours}"); count_update "${nours}"; # do they match (fatal if not)? check_match "${_next}" "${_ours}" "${repo}"; # could also check commit or count but this is direct # cool, print current status print_status BIN next "${repo}" "${next}" "${nnext}" "${_next}"; print_status BIN ours "${repo}" "${ours}" "${nours}" "${_ours}"; # save per-repository freshness (DO NOT CHANGE STORAGE ORDER!) OLD_list="${OLD_list} ${repo}:${OLD_this}"; done message "Generating local repository index" ./scripts/setup; message "Calculating topological package build order" temp=$(mktemp); ./scripts/deplist ${REPO} | ./scripts/depsort | cat -n > ${temp}; message "Calculating initial build requirements" LIST=$(git log --pretty=format:"%h" HEAD~${OLD_keep}..HEAD | while read commit; do git diff-tree --no-commit-id --name-only -r ${commit}; done | grep APKBUILD | cut -d/ -f-2 | sort | uniq | while read k; do grep -E "${k}$" ${temp}; done | sort -n | awk '{print $2}'); rm ${temp}; message "Going back in time by ${OLD_keep} commits"; git checkout "${BNCH}"~"${OLD_keep}"; for k in old new bin cmp; do rm -fr "${TEMP}"/${k}; mkdir "${TEMP}"/${k}; done message "Tracking starting state" for item in ${LIST}; do ( test -f "${item}"/APKBUILD || continue; # could be new package cd "${item}"; . ./APKBUILD; printf > "${TEMP}"/old/${item%/*}.${item#*/} "pkgname=%s\npkgver=%s\npkgrel=%s\n" "${pkgname}" "${pkgver}" "${pkgrel}"; ) done message "Going forward in time to present"; git checkout "${BNCH}"; for item in ${LIST}; do ( test -f "${item}"/APKBUILD || continue; # could be deleted package cd "${item}"; . ./APKBUILD; printf > "${TEMP}"/new/${item%/*}.${item#*/} "pkgname=%s\npkgver=%s\npkgrel=%s\n" "${pkgname}" "${pkgver}" "${pkgrel}"; ) done message "Verifying ALL pkgver/pkgrel are different"; FAIL=0; for item in ${LIST}; do ( if ! test -f "${TEMP}"/old/${item%/*}.${item#*/} || ! test -f "${TEMP}"/new/${item%/*}.${item#*/}; then continue; # not in both fi if ! cmp --silent "${TEMP}"/old/${item%/*}.${item#*/} "${TEMP}"/old/${item%/*}.${item#*/}; then printf "E: package '%s' needs a relbump!\n" "${item}"; FAIL=1; continue; fi if false; then . "${TEMP}"/old/${item%/*}.${item#*/}; old_pkgver=$pkgver; old_pkgrel=$pkgrel; . "${TEMP}"/new/${item%/*}.${item#*/}; new_pkgver=$pkgver; new_pkgrel=$pkgrel; # TODO: actually compare versions?? or not necessary?? fi ) done # THIS PROPERTY IS VERY IMPORTANT!! DO NOT PROCEED IF ANY PACKAGES WOULD BE CLOBBERED! test "${FAIL}" -eq 1 && printf "E: not all package versions are good (would clobber)!\n" && exit 1; message "Generating backup list of local package binaries" for repo in ${REPO}; do list_packages ${repo} > "${TEMP}"/manifest.${repo}.a; done message "Backing up APKINDEX files"; for repo in ${REPO}; do cp -p "${OURS}"/${repo}/${ARCH}/APKINDEX.tar.gz "${TEMP}"/APKINDEX.tar.gz.${repo}.bak; done message "Building packages for TESTING (STILL DANGEROUS!)" for item in ${LIST}; do ( cd "${item}"; # save state of binary package repository before build list_packages ${item%/*} > "${TEMP}"/build.a; # do the build and hope nothing gets clobbered! # TODO: can this be replaced by 'BOOTSTRAP=1 abuild -r'? do we need full control? # the current command does not clean up any files (like '-K') abuild sanitycheck clean deps unpack prepare mkusers build rootpkg index undeps; # save state of binary package repository after build list_packages ${item%/*} > "${TEMP}"/build.b; # compute delta and store to a file for later use comm -13 "${TEMP}"/build.a "${TEMP}"/build.b > "${TEMP}"/bin/${item%/*}.${item#*/}; ) done message "Moving newly-built packages to a less scary directory" for item in ${LIST}; do ( cd "${item}"; # get those toxic critters outta' here! while read k; do mv "${k}" "${TEMP}"/cmp/; done < "${TEMP}"/bin/${item%/*}.${item#*/}; ) done message "Clearing any stale cached files" for k in a b; do rm -f "${TEMP}"/list.${k}; done ## # NOTE: we CANNOT assume that there aren't multiple older versions # of a given package in "${OURS}", while we only care about the # "current" (latest) version that existed prior to the above build. # message "Scanning packages for shared library changes" for item in ${LIST}; do ( cd "${item}"; # get sorted list of .so files in all old .apk files for this package test ! -f "${TEMP}"/old/${item%/*}.${item#*/} && continue; # skip because it's new! ( . "${TEMP}"/old/${item%/*}.${item#*/}; find "${OURS}"/${item%/*}/${ARCH} -type f -name "${pkgname}-*${pkgver}-r${pkgrel}.apk" | while read k; do tar -tzf "${k}"; done | grep \\.so | sort >> "${TEMP}"/list.a; ) # get sorted list of .so files in all new .apk files for this package # FIXME: what happens if a package is deleted? still shows up in 'LIST' or not? ( . "${TEMP}"/new/${item%/*}.${item#*/}; find "${TEMP}"/cmp -type f -name "${pkgname}-*${pkgver}-r${pkgrel}.apk" | while read k; do tar -tzf "${k}"; done | grep \\.so | sort >> "${TEMP}"/list.b; ) ) done ## # FIXME: this needs to be done anyway, but this step is ALSO # a shortcut/kludge to avoid manually updating the index files # after moving the newly-built binaries out of "${OURS}". # After this step, there should be no visible before/after. # # NOTE: this is a full swap so that we also have a copy of # the "new" APKINDEX for later analysis. # message "Restoring original APKINDEX files"; for repo in ${REPO}; do cp -p "${OURS}"/${repo}/${ARCH}/APKINDEX.tar.gz "${TEMP}"/APKINDEX.tar.gz.${repo}.tmp; mv "${TEMP}"/APKINDEX.tar.gz.${repo}.bak "${OURS}"/${repo}/${ARCH}/APKINDEX.tar.gz; mv "${TEMP}"/APKINDEX.tar.gz.${repo}.tmp "${TEMP}"/APKINDEX.tar.gz.${repo}; done message "Sorting list of changed shared libraries" for k in a b; do sort "${TEMP}"/list.${k} | uniq > "${TEMP}"/list.${k}.sort; done ## # NOTE: the '-q' to 'apk' gives us package names without # version information. It may be possible for multiple # versions to exist, and only one be affected, and that only # the latest one could be affected, and that we already store # the "old" version information, ... but it doesn't matter # anyway since that information is derived from a binary repo # and we only care about the source repo from here forward. # # The '-q' also removes the "blah is required by..." line. # # TODO: don't actually call 'apk' here (slow); parse the # APKINDEX data and go from there. # message "Tracing dependencies for each changed shared library" comm -23 "${TEMP}"/list.a.sort "${TEMP}"/list.b.sort | while read file; do apk search -rq so:${file##*/}; done > "${TEMP}"/deps message "Finding parents of each affected package" sort "${TEMP}"/deps | while read name; do grep ${name} scripts/.index; # generated by 'setup' done | awk '{print $1}' | sort | uniq > "${TEMP}"/parents; ## # TODO: since we cannot know which repository(ies) provide the # possibly-affected package(s), we must search all repositories # for all occurrences. # message "Finding all APKBUILD files for each parent" NEED=""; while read name; do for repo in ${REPO}; do if test -f ${repo}/${name}/APKBUILD; then NEED="${NEED} ${repo}/${name}"; fi done done < "${TEMP}"/parents; message "Removing any duplicate entries" printf "%s\n" "${LIST}" | sort > "${TEMP}"/LIST; printf "%s\n" "${NEED}" | tr ' ' '\n' | sort > "${TEMP}"/NEED; comm -13 "${TEMP}"/LIST "${TEMP}"/NEED; echo $TEMP; message "Generating change list of local package binaries" for repo in ${REPO}; do list_packages ${repo} > "${TEMP}"/manifest.${repo}.b; done message "Comparing backup and change lists (sanity check)" for repo in ${REPO}; do diff -ur "${TEMP}"/manifest.${repo}.a "${TEMP}"/manifest.${repo}.b; done message "Cleaning up" #rm -fr "${TEMP}";