#!/bin/bash # Copyright 1999-2014 Gentoo Foundation # Distributed under the terms of the GNU General Public License v2 CATALYST_CONFIG=/etc/catalyst/catalyst.conf # Probe the default source dir from this script name. REPO_DIR=$(dirname "$(dirname "$(realpath "$0")")") # Set up defaults that config files can override if they want. SUBARCH=$(uname -m) EMAIL_TO="releng@gentoo.org,gentoo-releng-autobuilds@lists.gentoo.org" # Use full hostname by default as Gentoo servers will reject short names. EMAIL_FROM="catalyst@$(hostname -f)" EMAIL_SUBJECT_PREPEND="[${SUBARCH}-auto]" # Variables updated by command line arguments. declare -a config_files config_files=() verbose=0 keep_tmpdir=0 testing=0 preclean=0 lastrun=0 lock_file= parallel_sets=1 nonetwork=0 usage() { local msg=$1 if [ -n "${msg}" ]; then printf "%b\n\n" "${msg}" fi cat <] [-v|--verbose] [-h|--help] Options: -c|--config Specifies the config file to use (required) -C|--preclean Clean up loose artifacts from previous runs -j|--jobs Build spec sets in parallel -v|--verbose Send output of commands to console as well as log -k|--keep-tmpdir Don't remove temp dir when build finishes -t|--test Stop after mangling specs and copying files --interval Exit if last successful run was less than ago -l|--lock File to grab a lock on to prevent multiple invocations -X|--nonetwork Do not perform network operations (like uploading result) -h|--help Show this message and quit EOH } send_email() { if [[ ${verbose} -ge 1 ]]; then echo "$1 $2 build log at $3" fi [[ ${nonetwork} == 0 ]] || return [[ ! -z ${EMAIL_TO} ]] || return local subject="${EMAIL_SUBJECT_PREPEND} $1" local message=$2 local logfile=$3 local body if [ -n "${logfile}" ]; then body=$(printf '%b\n\n\n' "${message}"; tail -n 1000 "${logfile}"; printf '\n\n\nFull build log at %s\n' "${logfile}") else body=${message} fi printf 'From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%b' \ "${EMAIL_FROM}" "${EMAIL_TO}" "${subject}" "${body}" | \ /usr/sbin/sendmail -f "${EMAIL_FROM}" ${EMAIL_TO//,/ } } # Usage: run_cmd run_cmd() { local logfile="$1" shift echo "*** Running command: $*" &>> "${logfile}" if [[ ${verbose} == 2 ]]; then echo "*** Running command: $*" "$@" 2>&1 | tee -a "${logfile}" elif [[ ${verbose} == 1 ]]; then echo "*** Running command: $*" "$@" &>> "${logfile}" else "$@" &>> "${logfile}" fi # If we used tee above, make sure we pass back up the command's error. return ${PIPESTATUS[0]} } update_symlinks() { # This is a skeleton function that you can override from the config file. # It will be called by pre_build and after completing the build of a set # to ensure the symlinks point to the latest built stages. : } pre_build() { # This is a skeleton function that you can override from the config file. # It will be executed before the build is started. update_symlinks } post_build() { # This is a skeleton function that you can override from the config file. # It will be executed after the build is successfully completed. You can # use this to rsync the builds to another box : } catalyst_var() { # Extract a setting from the catalyst.conf. local var=$(grep --color=never -Po "^${1}\s*=\s*\K.*" "${CATALYST_CONFIG}" || true) local vas=$(echo $var|sed -e 's:^"::g' -e 's:"$::g') [[ -z ${vas} ]] || echo "${vas}" } trigger_post_build() { local set=$1 spec=$2 if ! run_cmd "${TMPDIR}/log/post_build.log" post_build "${set}" "${spec}"; then send_email "Catalyst build error - post_build" "The post_build function failed" "${TMPDIR}/log/post_build.log" exit 1 fi } parse_args() { local a while [[ $# -gt 0 ]] ; do a=$1 shift case "${a}" in -h|--help) usage exit 0 ;; -c|--config) config_files+=("$1") shift ;; -j|--jobs) parallel_sets="$1" shift ;; -v|--verbose) verbose=$(($verbose+1)) ;; -k|--keep-tmpdir) keep_tmpdir=1 ;; -t|--test) testing=1 ;; -C|--preclean) preclean=1 ;; -X|--nonetwork) nonetwork=1 ;; --interval) lastrun=$1 shift ;; -l|--lock) lock_file=$1 shift ;; -*) usage "ERROR: You have specified an invalid option: ${a}" exit 1 ;; *) usage "ERROR: This script takes no arguments: ${a}" exit 1 ;; esac done } give_latest_from_dates() { sed 's,-20,~20,g' | \ sort -k +1 -n -t '~' | \ awk -F'~' \ 'BEGIN{i=$1; o=$0}; { if($1 != i && i != "") { print o; }; i=$1; o=$0; } END { print o; };' | \ tr '~' '-' } # Replace the date/time stamp in the filename to "latest". # Forms we handle: # stage3-xxx-2018.0.tar.bz2 # stage3-xxx-20180116.tar.bz2 # stage3-xxx-20180116T015819Z.tar.bz2 convert_filename() { sed -E 's:-20[0-9]+(\.[0-9]+|T[0-9]+Z)?:-latest:g' } # Let's get our own namespaces/etc... to avoid leaking crap. containerize() { # If we've already relaunched, nothing to do. if [[ ${UNSHARE} == "true" ]] ; then return fi # Most systems have unshare available, but just in case. if type -P unshare >&/dev/null ; then local uargs=() # Probe the namespaces as some can be disabled (or we are not root). unshare -m -- true >&/dev/null && uargs+=( -m ) unshare -u -- true >&/dev/null && uargs+=( -u ) unshare -i -- true >&/dev/null && uargs+=( -i ) unshare -p -- true >&/dev/null && uargs+=( -p -f --mount-proc ) # Re-exec ourselves in the new namespace. UNSHARE=true exec unshare "${uargs[@]}" -- "$0" "$@" fi } # Update the git repo if possible. It might modify this script which will probably # make bash fail (since bash parses as it executes). So we have to safely re-exec # the script whenever there's an update. git_update() { # If we've already relaunched, nothing to do. if [[ ${GIT_UPDATE} == "true" || ${nonetwork} == 1 ]] ; then return fi pushd "${REPO_DIR}" >/dev/null git fetch -q revs=$(git rev-list HEAD..FETCH_HEAD) popd >/dev/null if [[ -n ${revs} ]] ; then GIT_UPDATE=true exec bash -c ' repo_dir=$1 script=$2 shift 2 pushd "${repo_dir}" >/dev/null git merge -q FETCH_HEAD || echo "${script}: WARNING: git repo is dirty" popd >/dev/null exec "${script}" "$@" ' -- "${REPO_DIR}" "$0" "$@" fi } # Stages are uploaded to @releng-incoming.gentoo.org and in order to # allow us to change what system this domain points to, we will retrieve the # SSH fingerprint from DNS. To do this securely, we need to ensure DNSSEC is # working. verify_dnssec() { [[ ${nonetwork} == 0 ]] || return which dig >/dev/null || { echo "net-dns/bind-tools is needed to verify DNSSEC is working" exit 1 } if ! dig +noall +comments dev.gentoo.org. IN SSHFP | egrep -q '^;; flags: [ a-z]+\'; then echo "DNSSEC does not appear to be working. Bailing out" exit 1 fi if ! grep -q '^options\>.*\' /etc/resolv.conf; then echo "DNSSEC is not enabled in /etc/resolv.conf" exit 1 fi } upload() { if [[ ${nonetwork} == 0 ]]; then echo Uploading "$@" local SSH_CMD=( ssh -i ${UPLOAD_KEY} -o UserKnownHostsFile=/dev/null -o VerifyHostKeyDNS=yes -o StrictHostKeyChecking=no -o IPQoS=cs0 ) local RSYNC_OPTS=( -e "${SSH_CMD[*]}" --archive --omit-dir-times --delay-updates ) rsync "${RSYNC_OPTS[@]}" "$@" ${UPLOAD_USER}@releng-incoming.gentoo.org:${UPLOAD_DEST} else echo Would now upload "$@" ls -l $@ fi } upsync_binpackages() { # parameter 1: a PKGDIR on the local host # parameter 2: the target dir in the mirroring system, should be of the # form arch/profileversion/name (e.g., amd64/17.0/x32 ) if [[ ${nonetwork} == 0 ]]; then echo Upsyncing binpackages from $1 to $2 local SSH_CMD=( ssh -i ${UPLOAD_KEY} -o UserKnownHostsFile=/dev/null -o VerifyHostKeyDNS=yes -o StrictHostKeyChecking=no -o IPQoS=cs0 ) local RSYNC_OPTS=( -e "${SSH_CMD[*]}" --archive --delete --delete-after --omit-dir-times --delay-updates --mkpath --min-size=1 --checksum --stats ) rsync "${RSYNC_OPTS[@]}" "$1"/* "${UPLOAD_USER}@releng-incoming.gentoo.org:/release/weekly/binpackages/$2/" else echo Would now upsync binpackages from $1 to $2 ls -l $@ fi } run_catalyst_commands() { doneconfig=0 for config_file in "${config_files[@]}"; do # Make sure all required values were specified. if [[ -z "${config_file}" || ! -e "${config_file}" ]]; then usage "ERROR: You must specify a valid config file to use: '$config_file' is not valid" exit 1 fi source "${config_file}" doneconfig=1 done if [[ ${doneconfig} == 0 ]]; then usage "ERROR: You must specify at least one valid config file to use" exit 1 fi # Some configs will set this explicitly, so don't clobber it. : ${BUILD_SRCDIR_BASE:=$(catalyst_var storedir)} : ${BUILD_SRCDIR_BASE:=/var/tmp/catalyst} # See if we had a recent success. if [[ ${lastrun} != 0 ]]; then last_success_file="${BUILD_SRCDIR_BASE}/.last_success" delay=$(( lastrun * 24 * 60 * 60 )) last_success=$(head -1 "${last_success_file}" 2>/dev/null || echo 0) if [[ $(date +%s) -lt $(( last_success + delay )) ]]; then exit 0 fi fi # Nuke any previous tmpdirs to keep them from accumulating. if [[ ${preclean} == 1 ]]; then rm -rf "${TMP_PATH:-/tmp}/catalyst-auto".* pushd "${BUILD_SRCDIR_BASE}" >/dev/null || exit 1 rm -rf --one-file-system \ kerncache packages snapshots tmp popd >/dev/null fi if catalyst --help | grep -q "git-treeish"; then snapshot_log=$(mktemp --tmpdir="${TMP_PATH:-/tmp}") if ! run_cmd "${snapshot_log}" catalyst -c "${CATALYST_CONFIG}" -s stable; then send_email "Catalyst build error - snapshot" "" "${snapshot_log}" exit 1 fi read TREEISH gitdir <<<$(egrep -o 'Creating .* tree snapshot [0-9a-f]{40} from .*' "${snapshot_log}" | cut -d' ' -f5,7) TIMESTAMP=$(git -C "${gitdir}" show --no-patch --format=%cd --date=format:%Y%m%dT%H%M%SZ "${TREEISH}") else TIMESTAMP=$(date -u +%Y%m%dT%H%M%SZ) snapshot_log=$(mktemp --tmpdir="${TMP_PATH:-/tmp}") if ! run_cmd "${snapshot_log}" catalyst -c "${CATALYST_CONFIG}" -s "${TIMESTAMP}"; then send_email "Catalyst build error - snapshot" "" "${snapshot_log}" exit 1 fi fi DATESTAMP=$(date -u +%Y%m%d) TMPDIR=$(mktemp -d --tmpdir="${TMP_PATH:-/tmp}" "catalyst-auto.${TIMESTAMP}.XXXXXX") if [[ ${verbose} -ge 1 ]]; then echo "TMPDIR = ${TMPDIR}" echo "TIMESTAMP = ${TIMESTAMP}" fi if ! mkdir -p "${TMPDIR}"/{specs,kconfig,log}; then echo "Couldn't create tempdirs!" exit 1 fi mv "${snapshot_log}" "${TMPDIR}/log/snapshot.log" if ! run_cmd "${TMPDIR}/log/pre_build.log" pre_build; then send_email "Catalyst build error - pre_build" "The pre_build function failed" "${TMPDIR}/log/pre_build.log" exit 1 fi cd "${SPECS_DIR}" || exit 1 for a in "" ${SETS}; do if [[ -z "${a}" ]]; then specs_var="SPECS" optional_specs_var="OPTIONAL_SPECS" else specs_var="SET_${a}_SPECS" optional_specs_var="SET_${a}_OPTIONAL_SPECS" fi for i in ${!specs_var} ${!optional_specs_var}; do cp --parents "${i}" "${TMPDIR}"/specs/ done done [[ -n ${KCONFIG_DIR} ]] && find "${KCONFIG_DIR}" -type f -exec cp {} "${TMPDIR}"/kconfig \; cd "${TMPDIR}/specs" || exit 1 # Fix up specs with datestamp for i in $(find -name '*.spec'); do kconfig_lines=$(grep '^boot/kernel/[^/]\+/config:' "${i}") if [[ -n ${kconfig_lines} ]]; then echo "${kconfig_lines}" | while read line; do key=$(echo "${line}" | cut -d: -f1) filename=$(basename $(echo "${line}" | cut -d: -f2)) sed -i "s|^${key}:.*\$|${key}: ${TMPDIR}/kconfig/${filename}|" "${i}" done fi # Expand vars that the spec expects us to. sed -i \ -e "s:@TIMESTAMP@:${TIMESTAMP}:g" \ -e "s:@REPO_DIR@:${REPO_DIR}:g" \ -e "s:@TREEISH@:${TREEISH}:g" \ "${i}" done if [[ ${testing} == 1 ]]; then echo "Exiting due to --test" exit fi build_failure=0 # bash built-in time function is not used if ! which time >/dev/null ; then timeprefix=() echo "sys-process/time is optional for build resource utilization" else timeprefix=( "time" ) fi JOB_PIDS=() JOB_RETS=() JOB_IDX_S=0 JOB_IDX_E=0 for a in "" ${SETS}; do if [[ $(( JOB_IDX_E - JOB_IDX_S )) == ${parallel_sets} ]] ; then wait ${JOB_PIDS[$(( JOB_IDX_S++ ))]} JOB_RETS+=( $? ) fi ( if [[ -z ${a} ]]; then specs_var="SPECS" optional_specs_var="OPTIONAL_SPECS" else specs_var="SET_${a}_SPECS" optional_specs_var="SET_${a}_OPTIONAL_SPECS" fi for i in ${!specs_var}; do LOGFILE="${TMPDIR}/log/$(echo "${i}" | sed -e 's:/:_:' -e 's:\.spec$::').log" specpath=$(readlink -f "${i}") run_cmd "${LOGFILE}" "${timeprefix[@]}" catalyst -a -c "${CATALYST_CONFIG}" -f "${specpath}" if [[ $? != 0 ]]; then build_failure=1 send_email "Catalyst fatal build error - ${i}" "" "${LOGFILE}" exit 1 else trigger_post_build "${a}" "${i}" fi done for i in ${!optional_specs_var}; do LOGFILE="${TMPDIR}/log/$(echo "${i}" | sed -e 's:/:_:' -e 's:\.spec$::').log" specpath=$(readlink -f "${i}") run_cmd "${LOGFILE}" "${timeprefix[@]}" catalyst -a -c "${CATALYST_CONFIG}" -f "${specpath}" if [[ $? != 0 ]]; then build_failure=1 send_email "Catalyst non-fatal build error - ${i}" "" "${LOGFILE}" break else trigger_post_build "${a}" "${i}" fi done # Do not purge yet, because there might be interdendency between specs # in different build sets! update_symlinks exit ${build_failure} )& JOB_PIDS+=( $! ) : $(( ++JOB_IDX_E )) done for (( i = JOB_IDX_S; i < JOB_IDX_E; ++i )) ; do wait ${JOB_PIDS[i]} JOB_RETS+=( $? ) done build_failure=$(( 0 ${JOB_RETS[@]/#/+} )) # Now do the cleanup for a in "" ${SETS}; do if [[ -z ${a} ]]; then specs_var="SPECS" optional_specs_var="OPTIONAL_SPECS" else specs_var="SET_${a}_SPECS" optional_specs_var="SET_${a}_OPTIONAL_SPECS" fi for i in ${!specs_var} ${!optional_specs_var}; do LOGFILE="${TMPDIR}/log/$(echo "${i}" | sed -e 's:/:_:' -e 's:\.spec$::')_purge.log" specpath=$(readlink -f "${i}") # Bail out of cleaning (eg) stage3-openrc.spec failure if # CLST_AUTO_NOCLEAN="stage3-openrc.spec" is set run_cmd "${LOGFILE}" "${timeprefix[@]}" echo "Testing ${i} against CLST_AUTO_NOCLEAN=${CLST_AUTO_NOCLEAN}" [[ ${CLST_AUTO_NOCLEAN} == *${i}* ]] || run_cmd "${LOGFILE}" "${timeprefix[@]}" catalyst --purgetmponly -c "${CATALYST_CONFIG}" -f "${specpath}" done update_symlinks done trigger_post_build if [[ ${build_failure} == 0 ]]; then if [[ ${lastrun} != 0 ]]; then stamp=$(date) (date -d"${stamp}" +%s; echo "${stamp}") >"${last_success_file}" fi send_email "Catalyst build success" "Build process complete." "${TMPDIR}/log/post_build.log" if [[ ${keep_tmpdir} == 0 ]]; then if ! rm -rf "${TMPDIR}"; then echo "Could not remove tmpdir ${TMPDIR}!" exit 1 fi fi fi } main() { # Set pipefail so that run_cmd returns the right value in $?. set -o pipefail # Parse user arguments before we try doing container logic. parse_args "$@" # Update the release git dir if possible. git_update "$@" # Verify DNSSEC works verify_dnssec # Try to isolate ourselves from the rest of the system. containerize "$@" ( if [[ -n ${lock_file} ]]; then if ! flock -n 9; then echo "catalyst-auto already running" exit 1 fi fi run_catalyst_commands ) 9>"${lock_file:-/dev/null}" } main "$@"