summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/update500
1 files changed, 500 insertions, 0 deletions
diff --git a/scripts/update b/scripts/update
new file mode 100755
index 000000000..f1f02c238
--- /dev/null
+++ b/scripts/update
@@ -0,0 +1,500 @@
+#!/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}";