#!/usr/bin/env bash # # Common error handling # exit_trap_cmds=() on_exit() { exit_trap_cmds+=( "$1" ) } run_exit_trap_cmds() { for cmd in "${exit_trap_cmds[@]}"; do eval "${cmd}" done } trap run_exit_trap_cmds EXIT warn() { >&2 echo "Warning: $*" } bail() { if [[ $# -gt 0 ]]; then >&2 echo "Error: $*" fi exit 1 } usage() { cat <&2 usage bail "$1" } # # Parse arguments # kernel_versions=() variants=() while [[ $# -gt 0 ]]; do case $1 in -a|--after) shift; gitrev_after_arg=$1 ;; -b|--before) shift; gitrev_before_arg=$1 ;; -v|--variant) shift; variants+=( "$1" ) ;; -o|--output-dir) shift; output_dir=$1 ;; -r|--resume) shift; resume=1 ;; -h|--help) usage; exit 0 ;; *) usage_error "Invalid option '$1'" ;; esac shift done if [[ ${#variants[@]} -eq 0 ]]; then variants=( aws-k8s-1.23 metal-k8s-1.23 aws-dev metal-dev ) fi for var in "${variants[@]}"; do [[ -d variants/${var} ]] || bail "Unknown variant '${var}'" done readonly variants [[ -n ${output_dir} ]] || usage_error 'require -o|--output-dir' [[ -e ${output_dir} && ! -v resume ]] && bail "Output directory '${output_dir}' exists already, not touching it" readonly output_dir # Validate and resolve the given before and after Git revisions. Resolving # them now prevents relative references from moving around after the first # checkout. [[ -n ${gitrev_before_arg} ]] || usage_error 'require -b|--before' [[ -n ${gitrev_after_arg} ]] || usage_error 'require -a|--after' gitrev_before=$(git rev-parse --verify --end-of-options "${gitrev_before_arg}^{commit}") gitrev_after=$(git rev-parse --verify --end-of-options "${gitrev_after_arg}^{commit}") [[ -n ${gitrev_before} ]] || bail "Invalid Git revision '${gitrev_before_arg}'" [[ -n ${gitrev_after} ]] || bail "Invalid Git revision '${gitrev_after_arg}'" readonly gitrev_before readonly gitrev_after # # Prepare working tree # # We'll check out the before and after states to compare. For that the working # tree and the index need to be clean. if [[ -n $(git status --porcelain --untracked-files=no) ]]; then bail 'The working tree or index of the repository are not clean. ' \ 'Consider running "git stash" to temporarily stow away your changes.' fi # Restore current repository state whenever we exit (either a checked out # branch or the current detached head state). gitrev_original=$(git rev-parse --abbrev-ref HEAD) if [[ -z ${gitrev_original} ]]; then gitrev_original=$(git rev-parse HEAD) || bail 'Cannot determine current repository HEAD.' fi readonly gitrev_original on_exit "git checkout --quiet '${gitrev_original}'" # # Iterate over all viable build configurations in before and after states # mkdir -p "${output_dir}" || bail "Failed to create output directory '${output_dir}'" for state in after before; do gitrev_var=gitrev_${state} git checkout --quiet "${!gitrev_var}" || bail "Cannot check out '${!gitrev_var}'." for variant in "${variants[@]}"; do arches=() IFS=" " read -r -a arches <<< "$(grep "supported-arches" "variants/${variant}/Cargo.toml" | cut -d ' ' -f 3 | tr -d '"[]')" if [[ ${#arches[@]} -eq 0 ]]; then arches=( aarch64 x86_64 ) fi kver=$(grep "packages/kernel" "variants/${variant}/Cargo.toml" | cut -d ' ' -f 1 | cut -d '-' -f 2 | tr '_' '.') kernel_versions+=( "${kver}" ) for arch in "${arches[@]}"; do config_path=${output_dir}/config-${arch}-${variant}-${state} if [[ -v resume && -e ${config_path} ]]; then echo "${config_path} already exists, skipping" continue fi debug_id="state=${state} arch=${arch} variant=${variant} kernel=${kver}" IFS=- read -ra variant_parts <<<"${variant}" variant_platform="${variant_parts[0]}" variant_runtime="${variant_parts[1]}" variant_family="${variant_platform}-${variant_runtime}" # # Run build # cargo make \ -e BUILDSYS_ARCH="${arch}" \ -e BUILDSYS_VARIANT="${variant}" \ -e BUILDSYS_VARIANT_PLATFORM="${variant_platform}" \ -e BUILDSYS_VARIANT_RUNTIME="${variant_runtime}" \ -e BUILDSYS_VARIANT_FAMILY="${variant_family}" \ -e PACKAGE="kernel-${kver/./_}" \ build-package \ || bail "Build failed for ${debug_id}" # # Find kernel RPM # shopt -s nullglob kernel_rpms=( ./build/rpms/bottlerocket-*kernel-"${kver}"-"${kver}".*."${arch}".rpm ./build/rpms/bottlerocket-"${arch}"-*kernel-"${kver}"-"${kver}".*.rpm ) shopt -u nullglob case ${#kernel_rpms[@]} in 0) bail "No kernel RPM found for ${debug_id}" ;; 1) kernel_rpm=${kernel_rpms[0]} ;; *) # shellcheck disable=SC2012 # find(1) cannot sort by mtime kernel_rpm=$(ls -1t "${kernel_rpms[@]}" | head -n 1) warn "More than one kernel RPM found for ${debug_id}. Choosing '${kernel_rpm}' as the latest build." ;; esac kver_full=$(rpm --query --queryformat '%{VERSION}-%{RELEASE}' "${kernel_rpm}") # # Extract kernel config # rpm2cpio "${kernel_rpm}" \ | cpio --quiet --extract --to-stdout ./boot/config >"${config_path}" [[ -s "${config_path}" ]] || bail "Failed to extract config for ${debug_id}" echo "config-${arch}-${variant}-${state} -> ${kver_full}" >> "${output_dir}"/kver_mapping done # arch done # variant done # state # # Post-process the collected pairs of "before" and "after" configs (generate diffs, a report, a summary) # # Get the helpful diffconfig script from the kernel source tree. We package it # in the kernel-archive RPM from where it can be extracted. Here we extract the # latest version of the script, but any kernel version and arch will do. latest_kver=$(printf '%s\n' "${kernel_versions[@]}" | sort -V | tail -n1) latest_archive_rpms=( ./build/rpms/bottlerocket-*kernel-"${latest_kver}"-archive-*.rpm ) diffconfig=$(mktemp --suffix -bottlerocket-diffconfig) on_exit "rm '${diffconfig}'" rpm2cpio "${latest_archive_rpms[0]}" \ | cpio --quiet --extract --to-stdout \ | tar --xz --extract --to-stdout kernel-devel/scripts/diffconfig >"${diffconfig}" [[ -s ${diffconfig} ]] || bail "Failed to extract diffconfig tool from '${latest_archive_rpms[0]}'." chmod +x "${diffconfig}" # Diff the before and after states for each collected pair for config_before in "${output_dir}"/config-*-before; do config_after=${config_before/before/after} config_diff=${config_before/before/diff} "${diffconfig}" "${config_before}" "${config_after}" >"${config_diff}" \ || bail "Failed to diff '${config_before}' and '${config_after}'" done # Generate diff summary echo for config_diff in "${output_dir}"/config-*-diff; do config_base=${config_diff##*/} awk " /^-/ { removed += 1 } /^+/ { added += 1 } / -> / { changed += 1 } END { printf \"${config_base}:\t%3d removed, %3d added, %3d changed\n\", removed, added, changed } " "${config_diff}" done | sort -V echo # Generate combined report of changes head -v -n 999999 "${output_dir}"/*-diff >"${output_dir}"/diff-report echo "A full report has been placed in '${output_dir}/diff-report'" # Generate combined report in tabular form (csv) echo "config change" > "${output_dir}"/diff-table cat "${output_dir}"/*-diff | sort | uniq >> "${output_dir}"/diff-table for config_diff in "${output_dir}"/config-*-diff; do variant_name=$(echo "${config_diff}" | sed -e "s%^${output_dir}/config-%%" -e "s%-diff$%%") kver_before=$(grep "${variant_name}-before" "${output_dir}/kver_mapping" | cut -d ' ' -f 3) kver_after=$(grep "${variant_name}-after" "${output_dir}/kver_mapping" | cut -d ' ' -f 3) col_name="${variant_name} (${kver_before} -> ${kver_after})" sed -i "s/$/,/" "${output_dir}"/diff-table sed -i "/^config change/ s/$/${col_name}/" "${output_dir}"/diff-table mapfile -t diff_lines < "${config_diff}" for line in "${diff_lines[@]}"; do sed -i "/^${line}/ s/$/x/" "${output_dir}"/diff-table done done echo "A tabular report in csv-format has been placed in '${output_dir}/diff-table'"