diff options
Diffstat (limited to 'tools/perf/scripts/sync-headers.sh')
| -rwxr-xr-x | tools/perf/scripts/sync-headers.sh | 1676 |
1 files changed, 1676 insertions, 0 deletions
diff --git a/tools/perf/scripts/sync-headers.sh b/tools/perf/scripts/sync-headers.sh new file mode 100755 index 000000000000..01e2d108432f --- /dev/null +++ b/tools/perf/scripts/sync-headers.sh @@ -0,0 +1,1676 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Copyright (C) 2026, Red Hat Inc, Arnaldo Carvalho de Melo <acme@redhat.com> +# +# Sync tools/ header copies with the kernel sources, generate beauty +# string table diffs, verify perf still builds, and create git commits. + +set -euo pipefail + +YELLOW='\033[0;33m' +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +PERF_DIR=$(cd "$(dirname "$0")/.." && pwd) +KERNEL_DIR=$(cd "$PERF_DIR/../.." && pwd) +DRY_RUN=0 +BUILD=1 +COMMIT=1 +FORCE=0 +OUTPUT_DIR="${OUTPUT_DIR:-${TMPDIR:-/tmp}/build/perf-tools-$(id -u)}" +SIGN_OFF_NAME="${SIGN_OFF_NAME:-}" +SIGN_OFF_EMAIL="${SIGN_OFF_EMAIL:-}" +HEADER_FILTER="" +VERBOSE=0 + +# Track build successes and failures for end-of-run summary +declare -a SYNCED_OK=() +declare -a BUILD_FAILED=() +declare -a COPY_FAILED=() +declare -a COMMIT_FAILED=() +declare -a TOOLS_AHEAD=() +declare -a HAND_SYNC=() +declare -a MANUAL=() +declare -a FORCED=() + +info() { echo -e "${GREEN}==>${NC} $*"; } +warn() { echo -e "${YELLOW}Warning:${NC} $*"; } +err() { echo -e "${RED}Error:${NC} $*" >&2; } +detail(){ [[ $VERBOSE -eq 1 ]] && echo -e " $*"; return 0; } + +usage() { + local status="${1:-0}" + + cat <<EOF +Usage: $(basename "$0") [OPTIONS] + +Sync tools/ header copies with the kernel sources and create commits. + +Options: + -n, --dry-run Show what would be done without making changes + -N, --no-build Skip the perf build verification step + -C, --no-commit Copy files but do not create git commits + -f, --filter PAT Only process headers matching PAT (substring match + against the kernel-side path) + -F, --force Sync also when both copies have content the other + lacks, after manual investigation; tools/ additions + dropped are recorded in the commit + -o, --output DIR Build output directory (default: $OUTPUT_DIR) + -v, --verbose Show more details + -h, --help Show this help + +Environment: + SIGN_OFF_NAME Override git user.name for Signed-off-by + SIGN_OFF_EMAIL Override git user.email for Signed-off-by + OUTPUT_DIR Build output directory + +Through make: + SYNC_HEADERS_ARGS Options to pass to this script, e.g.: + make -C tools/perf sync-headers SYNC_HEADERS_ARGS='-n' + +The script: + 1. Runs 'make -C tools/perf check-headers' to find stale copies. + 2. For each stale header, identifies the kernel commits that changed it, + including the merges that brought content of their own. + 3. Copies the kernel header into the tools/ tree, skipping it when only + the tools/ copy changed (it is ahead) or when it carries intentional + edits, and stopping for manual investigation when both sides have + content the other lacks. + 4. For beauty headers, runs the relevant beauty script(s) before and + after, showing new/changed string table entries. + 5. Builds perf and the other consumers of the header to verify nothing + breaks. + 6. Creates a git commit per header (or group) with the standard commit + message format. + +EOF + exit "$status" +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -n|--dry-run) DRY_RUN=1; shift ;; + -N|--no-build) BUILD=0; shift ;; + -C|--no-commit) COMMIT=0; shift ;; + -f|--filter) + [[ $# -ge 2 ]] || { err "$1 needs an argument"; exit 1; } + HEADER_FILTER="$2"; shift 2 ;; + -F|--force) FORCE=1; shift ;; + -o|--output) + [[ $# -ge 2 ]] || { err "$1 needs an argument"; exit 1; } + OUTPUT_DIR="$2"; shift 2 ;; + -v|--verbose) VERBOSE=1; shift ;; + -h|--help) usage ;; + *) err "Unknown option: $1"; usage 1 ;; + esac +done + +# make -C resolves a relative O= from the directory it changes to, so pin +# the build directory down before main() moves into the kernel tree. +case "$OUTPUT_DIR" in +/*) ;; +*) OUTPUT_DIR="$PWD/$OUTPUT_DIR" ;; +esac + +sign_off() { + local name="${SIGN_OFF_NAME:-$(git config user.name)}" + local email="${SIGN_OFF_EMAIL:-$(git config user.email)}" + echo "Signed-off-by: $name <$email>" +} + +# --- Detect stale headers ------------------------------------------------ + +detect_stale_headers() { + local tmpfile + tmpfile=$(mktemp) + + # check-headers.sh must be run from tools/perf/ (it does cd ../.. internally) + (cd "$PERF_DIR" && bash check-headers.sh 2>/dev/null) | \ + grep '^ *diff -u ' | \ + sed 's/^ *diff -u //' > "$tmpfile" + + if [[ ! -s "$tmpfile" ]]; then + rm -f "$tmpfile" + return 1 + fi + + cat "$tmpfile" + rm -f "$tmpfile" + return 0 +} + +# --- Classify a header --------------------------------------------------- +# +# Returns via globals: +# HDR_CLASS: "files" | "beauty" +classify_header() { + local tools_path="$1" + + if [[ "$tools_path" == tools/perf/trace/beauty/* ]]; then + HDR_CLASS="beauty" + else + HDR_CLASS="files" + fi +} + +# --- Find kernel commits that changed a header since our copy ------------ + +# Copies checked with -I or hunk-ignore rules in check-headers.sh carry +# intentional edits, so a sync commit never leaves the two blobs equal. +has_ignore_rules() { + local kernel_path="$1" + local check_headers="$PERF_DIR/check-headers.sh" + + grep -Eq "^[[:space:]]*(check[[:space:]]+${kernel_path}[[:space:]]+.*-I|check_ignore_some_hunks[[:space:]]+${kernel_path}([[:space:]]|$))" \ + "$check_headers" 2>/dev/null && return 0 + sed -n '/^declare -a SYNC_CHECK_FILES=/,/)/p' "$check_headers" 2>/dev/null | \ + grep -Fq "\"$kernel_path\"" +} + +# The last commit that left the two copies equal: an in-tree kernel commit +# can update both files while an earlier kernel change is still missing. +find_sync_point() { + local kernel_path="$1" + local tools_path="$2" + local -a candidates blobs + local i + + # Files with ignore rules carry intentional edits and will never have + # identical blobs; fall back to the last commit touching tools/. + if ! has_ignore_rules "$kernel_path"; then + mapfile -t candidates < <(git log --no-merges --format='%H' -- "$kernel_path" "$tools_path" 2>/dev/null) + mapfile -t blobs < <( + printf '%s\n' "${candidates[@]}" | \ + awk -v k="$kernel_path" -v t="$tools_path" '{ print $1 ":" k ORS $1 ":" t }' | \ + git cat-file --batch-check) + + for ((i = 0; i < ${#candidates[@]}; i++)); do + # "<spec> missing" keeps the ':' of the spec as its first field, + # which no object id has. + if [[ "${blobs[2*i]%% *}" == "${blobs[2*i+1]%% *}" && \ + "${blobs[2*i]%% *}" != *:* ]]; then + echo "${candidates[i]}" + return + fi + done + fi + + git log --no-merges --format='%H' -1 -- "$tools_path" 2>/dev/null +} + +# A kernel commit that changed both the header and the tools copy already +# carried its change into tools/, so it has nothing left to pick up here. +find_kernel_commits() { + local kernel_path="$1" + local tools_path="$2" + local sync_point="$3" + local copied sha + + copied=$(git log --no-merges --format='%H' \ + "${sync_point}..HEAD" -- "$tools_path" 2>/dev/null) + [[ -n "$copied" ]] && copied+=$'\n' + + while IFS= read -r sha; do + [[ "$copied" == *"$sha"$'\n'* ]] && continue + echo "$sha" + done < <(git log --no-merges --format='%H' \ + "${sync_point}..HEAD" -- "$kernel_path" 2>/dev/null) +} + +# The lines the kernel copy has and the tools/ copy lacks, as "start,end" +# ranges on the kernel side. +added_line_ranges() { + local kernel_path="$1" + local tools_path="$2" + local out + + out=$(diff -U0 "$tools_path" "$kernel_path" 2>/dev/null) || true + printf '%s\n' "$out" | awk ' + /^@@/ { + split($3, a, ","); + start = substr(a[1], 2); + count = (a[2] == "" ? 1 : a[2]); + if (count > 0) + print start "," start + count - 1; + }' +} + +# A merge that resolves a conflict brings content of its own, e.g. a +# capability renumbered to avoid clashing with another one; git log +# --no-merges does not see those, so credit the merge with the lines it +# introduced. +merge_pickup_commits() { + local kernel_path="$1" + local tools_path="$2" + local sync_point="$3" + local range sha + local -a blame_args=() + local -a shas=() + + [[ -n "$sync_point" ]] || return 0 + + while read -r range; do + [[ -z "$range" ]] && continue + blame_args+=(-L "$range") + done < <(added_line_ranges "$kernel_path" "$tools_path") + + if [[ ${#blame_args[@]} -eq 0 ]]; then + return 0 + fi + + # Blame only what changed since the last sync, the lines older than + # it come out as the boundary commit and are not to be picked up. + while read -r sha; do + [[ -z "$sha" ]] && continue + shas+=("$sha") + done < <(git blame --porcelain "${sync_point}..HEAD" \ + "${blame_args[@]}" -- "$kernel_path" 2>/dev/null | + awk ' + /^[0-9a-f]{40} / { + sha = $1; + if (sha !~ /^0+$/) + order[++n] = sha; + next; + } + /^boundary$/ { + skip[sha] = 1; + next; + } + END { + for (i = 1; i <= n; i++) + if (!(order[i] in skip)) + print order[i]; + }') + + if [[ ${#shas[@]} -eq 0 ]]; then + return 0 + fi + + printf '%s\n' "${shas[@]}" | awk 'NF && !seen[$0]++' | + git rev-list --no-walk=unsorted --stdin --min-parents=2 2>/dev/null +} + +# Render commit ids with FORMAT at the usual 12 chars, keeping their order. +format_commits() { + local format="$1" + local ids="$2" + + [[ -n "$ids" ]] || return 0 + printf '%s\n' "$ids" | git log --no-walk=unsorted --abbrev=12 \ + --format="$format" --stdin 2>/dev/null +} + +# "add" when only the kernel changed since the sync point, "delete" when +# only the tools/ copy did, "both" when each side did; the sync point is +# where the blobs were equal, so comparing them is exact. +sync_direction() { + local kernel_path="$1" + local tools_path="$2" + local sync_point="$3" + local tools_base kernel_base tools_now kernel_now + + [[ -n "$sync_point" ]] || { echo add; return; } + tools_base=$(git -C "$KERNEL_DIR" rev-parse -q --verify "$sync_point:$tools_path") || { + echo add + return + } + kernel_base=$(git -C "$KERNEL_DIR" rev-parse -q --verify "$sync_point:$kernel_path") || { + echo add + return + } + tools_now=$(git -C "$KERNEL_DIR" hash-object --path="$tools_path" "$tools_path") || { + echo add + return + } + kernel_now=$(git -C "$KERNEL_DIR" hash-object --path="$kernel_path" "$kernel_path") || { + echo add + return + } + + if [[ "$tools_now" == "$tools_base" ]]; then + echo add # only the kernel moved + elif [[ "$kernel_now" == "$kernel_base" ]]; then + echo delete # only tools/ moved, it is ahead + else + echo both + fi +} + +# A clean merge that matches the kernel means the tools/ side changed only +# what the kernel already has, so a copy drops nothing unique. +merge_matches_kernel() { + local kernel_path="$1" + local tools_path="$2" + local sync_point="$3" + local base merged status + + [[ -n "$sync_point" ]] || return 1 + base=$(mktemp) || return 1 + merged=$(mktemp) || { rm -f "$base"; return 1; } + if ! git -C "$KERNEL_DIR" show "$sync_point:$tools_path" > "$base" 2>/dev/null; then + rm -f "$base" "$merged" + return 1 + fi + if ! git merge-file -p "$KERNEL_DIR/$tools_path" "$base" \ + "$KERNEL_DIR/$kernel_path" > "$merged" 2>/dev/null; then + rm -f "$base" "$merged" + return 1 + fi + cmp -s "$merged" "$KERNEL_DIR/$kernel_path" + status=$? + rm -f "$base" "$merged" + return $status +} + +# The additions only the tools/ copy added since the sync point, what a +# forced sync drops; diffing against the sync-point blob gives it exactly. +tools_only_lines() { + local tools_path="$1" + local sync_point="$2" + local line + + [[ -n "$sync_point" ]] || return 0 + git -C "$KERNEL_DIR" rev-parse -q --verify "$sync_point:$tools_path" \ + >/dev/null || return 0 + + while IFS= read -r line; do + case "$line" in + ---*|+++*) continue ;; + -*) echo " $line" ;; + esac + done < <(git -C "$KERNEL_DIR" cat-file blob "$sync_point:$tools_path" | \ + diff -u "$KERNEL_DIR/$tools_path" - 2>/dev/null) +} + +# --- Map a kernel header path to its beauty script(s) -------------------- +# +# The mapping comes from the beauty Build files: a script reads the +# headers listed as its prerequisites and the directories passed as its +# arguments. The header name still has to appear in the script so that +# one reading a whole tree, like syscalltbl.sh, doesn't match everything. + +declare -A BEAUTY_MAKE_DIRS=() +declare -A BEAUTY_MAKE_SCRIPTS=() +declare -A BEAUTY_SCRIPT_ARGS=() +declare -A BEAUTY_SCRIPT_PATH=() +declare -A BEAUTY_SCRIPT_ROOTS=() +declare -A BEAUTY_SCRIPT_FILES=() +declare -a BEAUTY_SCRIPTS=() +BEAUTY_ARCH="" + +beauty_add_script() { + local script="$1" + + [[ -n "$script" ]] || return 0 + [[ -n "${BEAUTY_SCRIPT_FILES[$script]+x}" ]] && return 0 + BEAUTY_SCRIPT_FILES["$script"]="" + BEAUTY_SCRIPT_ROOTS["$script"]="" + BEAUTY_SCRIPTS+=("$script") +} + +find_beauty_scripts() { + local kernel_path="$1" + local tools_path="$2" + local header_basename script script_path root matched + + header_basename=$(basename "$kernel_path") + + for script in "${BEAUTY_SCRIPTS[@]+"${BEAUTY_SCRIPTS[@]}"}"; do + script_path="${BEAUTY_SCRIPT_PATH[$script]:-$script}" + matched=0 + case $'\n'"${BEAUTY_SCRIPT_FILES[$script]:-}"$'\n' in + *$'\n'"$tools_path"$'\n'*) matched=1 ;; + esac + if [[ $matched -eq 0 ]]; then + while IFS= read -r root; do + [[ -n "$root" ]] || continue + [[ "$tools_path" == "$root" || \ + "$tools_path" == "$root"/* ]] || continue + if grep -F -q -e "/$header_basename" \ + -e "\"$header_basename\"" \ + "$KERNEL_DIR/$script_path" 2>/dev/null; then + matched=1 + break + fi + done <<< "${BEAUTY_SCRIPT_ROOTS[$script]:-}" + fi + [[ $matched -eq 1 ]] && echo "$script_path" + done +} + +# --- Find buildable targets that consume a header ------------------------- +# +# Given a tools/ header path, grep tools/ source files to find which +# buildable directories include it, then return them as build commands. +# This is discovered dynamically so new consumers are picked up automatically. + +# C sources include tools/ headers by their include-relative path, e.g. +# <linux/socket.h> for tools/include/uapi/linux/socket.h. +include_relative_path() { + local tools_path="$1" + + case "$tools_path" in + tools/include/uapi/*) + echo "${tools_path#tools/include/uapi/}" + ;; + tools/include/*) + echo "${tools_path#tools/include/}" + ;; + tools/arch/*/include/uapi/*) + echo "${tools_path#tools/arch/*/include/uapi/}" + ;; + tools/arch/*/include/*) + echo "${tools_path#tools/arch/*/include/}" + ;; + *) + basename "$tools_path" + ;; + esac +} + +# Map from a source file path to its buildable target directory and make command. +# Returns empty if the file doesn't belong to a known buildable target. +buildable_target() { + local src="$1" + case "$src" in + tools/perf/*) + echo "perf:$PERF_DIR" + ;; + tools/lib/bpf/*) + echo "libbpf:$KERNEL_DIR/tools/lib/bpf" + ;; + tools/bpf/bpftool/*) + echo "bpftool:$KERNEL_DIR/tools/bpf/bpftool" + ;; + tools/objtool/*) + echo "objtool:$KERNEL_DIR/tools/objtool" + ;; + tools/lib/subcmd/*) + echo "libsubcmd:$KERNEL_DIR/tools/lib/subcmd" + ;; + tools/lib/api/*) + echo "libapi:$KERNEL_DIR/tools/lib/api" + ;; + tools/lib/perf/*) + echo "libperf:$KERNEL_DIR/tools/lib/perf" + ;; + tools/lib/symbol/*) + echo "libsymbol:$KERNEL_DIR/tools/lib/symbol" + ;; + tools/tracing/*) + echo "tracing:$KERNEL_DIR/tools/tracing" + ;; + esac +} + +find_build_targets() { + local tools_path="$1" + local header_class="$2" + local include_path + local targets=() + local seen=() + + # Beauty copies are only on the include path of the beauty scripts. + if [[ "$header_class" == "beauty" ]]; then + echo "perf:$PERF_DIR" + return + fi + + include_path=$(include_relative_path "$tools_path") + + while IFS= read -r src; do + [[ -z "$src" ]] && continue + local target + target=$(buildable_target "$src") + [[ -z "$target" ]] && continue + + # Deduplicate + local already=0 + for s in "${seen[@]+"${seen[@]}"}"; do + if [[ "$s" == "$target" ]]; then + already=1 + break + fi + done + if [[ $already -eq 0 ]]; then + seen+=("$target") + targets+=("$target") + fi + done < <(grep -rlF -e "<$include_path>" -e "\"$include_path\"" -e "/$include_path" \ + --include='*.c' --include='*.h' --include='*.S' \ + "$KERNEL_DIR/tools/" 2>/dev/null | \ + sed "s|^$KERNEL_DIR/||" | grep -v '^tools/include/') + + # Always include perf + local has_perf=0 + for t in "${targets[@]+"${targets[@]}"}"; do + [[ "$t" == perf:* ]] && has_perf=1 + done + if [[ $has_perf -eq 0 ]]; then + targets+=("perf:$PERF_DIR") + fi + + printf '%s\n' "${targets[@]}" +} + +# Print the consumers that will be built for a header or group. +show_build_targets() { + local what="$1" + local target_list="$2" + + info "Build-testing consumers of $what:" + while IFS=: read -r name dir; do + [[ -z "$name" ]] && continue + echo " $name ($dir)" + done <<< "$target_list" +} + +# Keep build output out of the source tree; perf, tracing and objtool take +# O=, the libraries and bpftool take OUTPUT=. +build_target() { + local name="$1" + local dir="$2" + + case "$name" in + perf) + make -C "$dir" O="$OUTPUT_DIR" -j"$(nproc)" + ;; + tracing|objtool) + (umask 077 && mkdir -p "$OUTPUT_DIR/$name") + make -C "$dir" O="$OUTPUT_DIR/$name" -j"$(nproc)" + ;; + *) + (umask 077 && mkdir -p "$OUTPUT_DIR/$name") + make -C "$dir" OUTPUT="$OUTPUT_DIR/$name/" -j"$(nproc)" + ;; + esac +} + +# Run build tests for all targets that consume a header. +# Returns 0 if all builds pass, 1 if any fail. +build_test_targets() { + local tools_path="$1" + local header_class="$2" + local header_basename log_name + local target_list failed=0 + + header_basename=$(basename "$tools_path") + # Key the log by tools path, same-basename headers in different + # directories would otherwise share a log. + log_name="${tools_path//\//-}" + target_list=$(find_build_targets "$tools_path" "$header_class") + show_build_targets "$header_basename" "$target_list" + + while IFS=: read -r name dir; do + [[ -z "$name" ]] && continue + local log="$OUTPUT_DIR/$name.$log_name.log" + if ! build_target "$name" "$dir" > "$log" 2>&1; then + err " $name build failed, see $log" + failed=1 + fi + done <<< "$target_list" + + return $failed +} + +# --- Run a beauty script capturing output -------------------------------- +# +# The arguments come from the same Build files the build uses, so a new +# script or a changed directory needs no edit here; run bare, +# arch_errno_names.sh misses the header copies. + +# Resolve a Build variable token, $(srctree)/path or $(dir)/suffix, to a +# tree-relative path. +beauty_resolve_token() { + local token="$1" + local name suffix + + case "$token" in + '$(srctree)'*) + suffix="${token//\$(srctree)/}" + echo "${suffix#/}" + ;; + '$('*')'*) + name="${token#\$(}" + name="${name%%)*}" + suffix="${token#*\)}" + [[ -n "${BEAUTY_MAKE_DIRS[$name]:-}" ]] || return 1 + printf '%s\n' "${BEAUTY_MAKE_DIRS[$name]}$suffix" | \ + sed "s/\$(SRCARCH)/$BEAUTY_ARCH/g" + ;; + *) + return 1 + ;; + esac +} + +# Collect, from one Build file, the script each rule runs with its +# arguments and the headers it takes as prerequisites. +parse_beauty_build() { + local build="$1" + local -a rule_lines=() + local line last_rule="" var script args token value roots files needle + local name path re_dir re_tbl + + [[ -r "$build" ]] || return 0 + + # _dir :=, _tbl := assignments, and the $(SHELL) rules using them. + re_dir='^([A-Za-z0-9_]+_(dir|dirs))[[:space:]]*(:=|\?=)[[:space:]]*' + re_dir+='\$\(srctree\)/(.*)$' + re_tbl='^([A-Za-z0-9_]+_(tbl|tbls))[[:space:]]*(:=|\?=)[[:space:]]*' + re_tbl+='\$\(srctree\)/((.*\.sh))[[:space:]]*$' + + while IFS= read -r line; do + if [[ "$line" =~ $re_dir ]]; then + BEAUTY_MAKE_DIRS["${BASH_REMATCH[1]}"]=$(printf '%s' \ + "${BASH_REMATCH[4]%/}" | \ + sed "s/\$(SRCARCH)/$BEAUTY_ARCH/g") + elif [[ "$line" =~ $re_tbl ]]; then + BEAUTY_MAKE_SCRIPTS["${BASH_REMATCH[1]}"]="${BASH_REMATCH[4]}" + elif [[ "$line" == *'$(SHELL)'* && -n "$last_rule" ]]; then + rule_lines+=("$last_rule"$'\n'"$line") + elif [[ "$line" != '#'* && "$line" != $'\t'* && \ + "$line" == *:* && "$line" != *':='* && \ + "$line" != *'?='* && "$line" != *'+='* ]]; then + last_rule="$line" + fi + done < "$build" + + local rule prereqs command + for rule in "${rule_lines[@]+"${rule_lines[@]}"}"; do + prereqs="${rule%%$'\n'*}" + command="${rule#*$'\n'}" + + script="" + for var in "${!BEAUTY_MAKE_SCRIPTS[@]}"; do + needle="'\$($var)'" + if [[ "$command" == *"$needle"* ]]; then + script="${BEAUTY_MAKE_SCRIPTS[$var]##*/}" + BEAUTY_SCRIPT_PATH["$script"]="${BEAUTY_MAKE_SCRIPTS[$var]}" + args="${command#*"$needle"}" + break + fi + done + [[ -n "$script" ]] || continue + beauty_add_script "$script" + + args="${args//\'}" + args=$(printf '%s\n' "$args" | \ + sed 's/\$(patsubst -%,,\$(CC))/gcc/g') + args="${args%' > $@'}" + value="" + roots="" + for token in $args; do + case "$token" in + '$@') + token='@OUTFILE@' + ;; + '$(srctree)'*|'$('*')'*) + path=$(beauty_resolve_token "$token") || { + value="" + break + } + case "$token" in + '$(srctree)'*) + roots+="${path}"$'\n' + ;; + *) + name="${token#\$(}" + name="${name%%)*}" + roots+="${BEAUTY_MAKE_DIRS[$name]:-}"$'\n' + ;; + esac + token="$KERNEL_DIR/$path" + ;; + *) ;; + esac + token=$(printf '%s' "$token" | sed "s/\$(SRCARCH)/$BEAUTY_ARCH/g") + if [[ "$token" == *'$('* ]]; then + value="" + break + fi + value+="${value:+ }$token" + done + BEAUTY_SCRIPT_ROOTS["$script"]+="$roots" + [[ -n "$value" ]] && BEAUTY_SCRIPT_ARGS["$script"]="$value" + + prereqs="${prereqs#*:}" + files="" + for token in $prereqs; do + path=$(beauty_resolve_token "$token") || continue + [[ "$path" == *'*'* ]] && continue + [[ -n "$path" ]] && files+="$path"$'\n' + done + BEAUTY_SCRIPT_FILES["$script"]+="$files" + done +} + +setup_beauty_args() { + local build found=0 + + BEAUTY_ARCH=$(uname -m) + case "$BEAUTY_ARCH" in + x86_64|i[3-6]86) BEAUTY_ARCH=x86 ;; + aarch64|arm64) BEAUTY_ARCH=arm64 ;; + s390*) BEAUTY_ARCH=s390 ;; + riscv*) BEAUTY_ARCH=riscv ;; + loongarch*) BEAUTY_ARCH=loongarch ;; + ppc*) BEAUTY_ARCH=powerpc ;; + sparc*) BEAUTY_ARCH=sparc ;; + mips*) BEAUTY_ARCH=mips ;; + esac + + for build in "$PERF_DIR/trace/beauty/Build" \ + "$PERF_DIR/trace/beauty/tracepoints/Build"; do + [[ -r "$build" ]] || continue + parse_beauty_build "$build" + found=1 + done + + [[ $found -eq 1 ]] || \ + detail "No beauty Build file, running the scripts without arguments" +} + +run_beauty_script() { + local script="$1" + local -a cmd=() + local outfile="" i + + read -ra cmd <<< "${BEAUTY_SCRIPT_ARGS["${script##*/}"]:-}" + for ((i = 0; i < ${#cmd[@]}; i++)); do + if [[ "${cmd[$i]}" == '@OUTFILE@' ]]; then + outfile=$(mktemp) || continue + cmd[$i]="$outfile" + fi + done + bash "$script" "${cmd[@]+"${cmd[@]}"}" 2>/dev/null || true + if [[ -n "$outfile" ]]; then + cat "$outfile" 2>/dev/null || true + rm -f "$outfile" + fi +} + +# Run one beauty script against the kernel content swapped into the tools +# copy, capturing its output before and after; the saved copy restores the +# tools copy, also when the script is interrupted. +run_beauty_pass() { + local script="$1" + local kernel_path="$2" + local tools_path="$3" + local tmpdir="$4" + local idx="$5" + local before="$tmpdir/beauty.$idx.before" + local after="$tmpdir/beauty.$idx.after" + local saved="$tmpdir/beauty.$idx.saved" + local restore + + if ! cp -p "$KERNEL_DIR/$tools_path" "$saved"; then + err "Failed to save $tools_path" + return 1 + fi + restore=$(printf 'cp -p %q %q' "$saved" "$KERNEL_DIR/$tools_path") + # shellcheck disable=SC2064 # expand now, locals may be gone + trap "$restore" EXIT + # shellcheck disable=SC2064 # expand now, locals may be gone + trap "$restore; trap - EXIT; exit 130" INT + # shellcheck disable=SC2064 # expand now, locals may be gone + trap "$restore; trap - EXIT; exit 143" TERM + (cd "$KERNEL_DIR" && run_beauty_script "$script") > "$before" + if ! cp "$KERNEL_DIR/$kernel_path" "$KERNEL_DIR/$tools_path"; then + err "Failed to copy $kernel_path -> $tools_path" + cp -p "$saved" "$KERNEL_DIR/$tools_path" || \ + err "Failed to restore $tools_path" + trap - INT TERM EXIT + return 1 + fi + (cd "$KERNEL_DIR" && run_beauty_script "$script") > "$after" + if ! cp -p "$saved" "$KERNEL_DIR/$tools_path"; then + err "Failed to restore $tools_path" + trap - INT TERM EXIT + return 1 + fi + trap - INT TERM EXIT +} + +# The tools/perf/trace/beauty/ relative path of a beauty script, as shown +# in the commit messages. +beauty_script_relpath() { + echo "tools/perf/trace/beauty/${1##*/trace/beauty/}" +} + +# The commit message fragment for a beauty script whose table changed: +# the recipe the pre-pass ran and the difference between its two runs. +beauty_body_fragment() { + local script="$1" + local kernel_path="$2" + local tools_path="$3" + local before="$4" + local after="$5" + local rel_script args + + rel_script=$(beauty_script_relpath "$script") + # The runs are from the tree root, show the dirs relative to it. + args="${BEAUTY_SCRIPT_ARGS["${script##*/}"]:-}" + args="${args//"$KERNEL_DIR/"/}" + local before_args="$args" after_args="$args" + local before_tail=" > before.txt" after_tail=" > after.txt" + + # Scripts taking the output file as an argument write it themselves. + if [[ "$args" == *@OUTFILE@* ]]; then + before_args="${args//@OUTFILE@/out.before}" + after_args="${args//@OUTFILE@/out.after}" + before_tail="" + after_tail="" + fi + + printf '\n' + printf ' $ %s%s%s\n' "$rel_script" "${before_args:+ $before_args}" "$before_tail" + printf ' $ cp %s %s\n' "$kernel_path" "$tools_path" + printf ' $ %s%s%s\n' "$rel_script" "${after_args:+ $after_args}" "$after_tail" + if [[ "$args" == *@OUTFILE@* ]]; then + printf ' $ diff -u out.before out.after\n' + else + printf ' $ diff -u before.txt after.txt\n' + fi + printf '\n' + diff -u "$before" "$after" | tail -n +3 | sed 's/^/ /' + printf '\n' +} + +cleanup_tmpdir() { + [[ -n "$1" ]] || return 0 + rm -rf "$1" +} + +# --- Copy header and create commit --------------------------------------- + +process_header() { + local tools_path="$1" + local kernel_path="$2" + + classify_header "$tools_path" + + if [[ -n "$HEADER_FILTER" ]] && [[ "$kernel_path" != *"$HEADER_FILTER"* ]]; then + detail "Skipping $kernel_path (does not match filter)" + return + fi + + info "Processing: $kernel_path" + detail " Class: $HDR_CLASS" + detail " Tools: $tools_path" + detail " Kernel: $kernel_path" + + # Find kernel commits that changed this file + local sync_point pickup_commits="" + sync_point=$(find_sync_point "$kernel_path" "$tools_path") + if [[ -n "$sync_point" ]]; then + pickup_commits=$(find_kernel_commits "$kernel_path" \ + "$tools_path" "$sync_point") + else + warn "No previous sync commit found for $tools_path" + fi + + local kernel_commits="" + if [[ -n "$pickup_commits" ]]; then + kernel_commits=$(format_commits ' %h ("%s")' "$pickup_commits") + fi + + # A merge can bring content of its own; list it too, it is not the + # author of a change, so it gets no Cc. + local merge_commits="" + merge_commits=$(merge_pickup_commits "$kernel_path" "$tools_path" \ + "$sync_point") + if [[ -n "$merge_commits" ]]; then + if [[ -n "$kernel_commits" ]]; then + kernel_commits+=$'\n' + fi + kernel_commits+=$(format_commits ' %h ("%s")' "$merge_commits") + fi + + if [[ -n "$kernel_commits" ]]; then + echo "" + echo " Kernel commits since last sync:" + echo "$kernel_commits" + fi + + # Copies with ignore rules carry deliberate edits that a plain copy + # would drop; leave them for a hand sync, printing the Cc list for them. + if has_ignore_rules "$kernel_path"; then + local cc_lines="" + if [[ -n "$pickup_commits" ]]; then + cc_lines=$(format_commits 'Cc: %an <%ae>' "$pickup_commits" | sort -u) + if [[ -n "$cc_lines" ]]; then + echo "" + echo "$cc_lines" | sed 's/^/ /' + fi + echo "" + fi + warn "$tools_path carries intentional edits, sync by hand" + HAND_SYNC+=("$tools_path") + return + fi + + # The kernel is the reference, but only copy when that doesn't drop + # content only the tools/ copy has. + local direction dropped="" forced=0 + direction=$(sync_direction "$kernel_path" "$tools_path" "$sync_point") + case "$direction" in + delete) + warn "$tools_path is ahead of the kernel sources, leaving it alone" + TOOLS_AHEAD+=("$tools_path") + return + ;; + both) + if merge_matches_kernel "$kernel_path" "$tools_path" "$sync_point"; then + detail "Tools/ changes are already in the kernel" + elif [[ $FORCE -eq 0 ]]; then + err "$tools_path and $kernel_path both have content the other lacks, needs manual investigation" + MANUAL+=("$tools_path") + return + else + warn "Both copies have unique content, dropping the tools/ one (--force)" + dropped=$(tools_only_lines "$tools_path" "$sync_point") + forced=1 + fi + ;; + esac + + # For beauty files, capture the string table changes and keep the + # before/after files for the commit message. + local beauty_changed=0 + local beauty_tmpdir="" + local beauty_scripts + local -a beauty_script_list=() + beauty_scripts=$(find_beauty_scripts "$kernel_path" "$tools_path") + while IFS= read -r script; do + [[ -z "$script" ]] && continue + beauty_script_list+=("$script") + done <<< "$beauty_scripts" + if [[ ${#beauty_script_list[@]} -gt 0 ]]; then + beauty_tmpdir=$(mktemp -d) + + local idx=0 + for script in "${beauty_script_list[@]}"; do + local script_name + script_name=$(basename "$script") + local before="$beauty_tmpdir/beauty.$idx.before" + local after="$beauty_tmpdir/beauty.$idx.after" + + # The script reads the tools/ copy, so swap in the kernel one + # and restore the saved copy, also in a dry run. + if ! run_beauty_pass "$script" "$kernel_path" "$tools_path" \ + "$beauty_tmpdir" "$idx"; then + COPY_FAILED+=("$tools_path") + cleanup_tmpdir "$beauty_tmpdir" + return 1 + fi + + if ! diff -q "$before" "$after" > /dev/null 2>&1; then + echo "" + info "Beauty string changes from $script_name:" + diff -u "$before" "$after" | head -60 || true + beauty_changed=1 + fi + idx=$((idx + 1)) + done + fi + + if [[ $DRY_RUN -eq 1 ]]; then + [[ $forced -eq 1 ]] && FORCED+=("$tools_path") + cleanup_tmpdir "$beauty_tmpdir" + info "(dry run) Would copy $kernel_path -> $tools_path" + return + fi + + # Do the actual copy + if ! cp "$KERNEL_DIR/$kernel_path" "$KERNEL_DIR/$tools_path"; then + err "Failed to copy $kernel_path -> $tools_path" + COPY_FAILED+=("$tools_path") + cleanup_tmpdir "$beauty_tmpdir" + return 1 + fi + info "Copied $kernel_path -> $tools_path" + + # Verify the consumers still build + if [[ $BUILD -eq 1 ]]; then + if ! build_test_targets "$tools_path" "$HDR_CLASS"; then + err "Build failed after copying $kernel_path — reverting." + git -C "$KERNEL_DIR" checkout -- "$tools_path" || \ + err "Failed to revert $tools_path" + BUILD_FAILED+=("$tools_path") + cleanup_tmpdir "$beauty_tmpdir" + return 1 + fi + info "Build OK." + fi + + [[ $forced -eq 1 ]] && FORCED+=("$tools_path") + + if [[ $COMMIT -eq 0 ]]; then + SYNCED_OK+=("$tools_path") + cleanup_tmpdir "$beauty_tmpdir" + return + fi + + # Compose commit message + local header_short + header_short=$(basename "$kernel_path") + + # The subject follows the path of the copy, one wording per family. + local subject arch uapi_rel + case "$kernel_path" in + include/uapi/*) + uapi_rel="${kernel_path#include/uapi/}" + if [[ "$HDR_CLASS" == "beauty" ]]; then + subject="tools headers: Sync UAPI $uapi_rel with the kernel sources" + else + subject="tools headers UAPI: Sync $uapi_rel with the kernel sources" + fi + ;; + include/*) + subject="tools headers: Sync ${kernel_path#include/} with the kernel sources" + ;; + arch/*/include/uapi/*) + arch="${kernel_path#arch/}" + arch="${arch%%/*}" + subject="tools headers UAPI: Sync $arch/asm/${header_short} with the kernel sources" + ;; + arch/*) + arch="${kernel_path#arch/}" + arch="${arch%%/*}" + subject="tools arch $arch: Sync the $header_short copy with the kernel sources" + ;; + *) + subject="tools headers: Sync $kernel_path with the kernel sources" + ;; + esac + + # Build commit body + local body="" + + if [[ -n "$kernel_commits" ]]; then + body+="To pick up the changes in:" + body+=$'\n' + body+=$'\n' + body+="$kernel_commits" + body+=$'\n' + fi + + if [[ $beauty_changed -eq 1 ]]; then + # Show the beauty diff in the commit message. + body+=$'\n' + body+="That introduce these new entries in the string tables:" + body+=$'\n' + + local idx=0 + for script in "${beauty_script_list[@]}"; do + local before_msg="$beauty_tmpdir/beauty.$idx.before" + local after_msg="$beauty_tmpdir/beauty.$idx.after" + + if ! diff -q "$before_msg" "$after_msg" > /dev/null 2>&1; then + body+=$(beauty_body_fragment "$script" "$kernel_path" \ + "$tools_path" "$before_msg" "$after_msg") + body+=$'\n' + fi + idx=$((idx + 1)) + done + elif [[ ${#beauty_script_list[@]} -gt 0 ]]; then + body+=$'\n' + body+="That don't result in changes to the string tables generated" + body+=" from this header." + else + body+=$'\n' + body+="That just rebuilds perf, silencing this build warning." + fi + + if [[ -n "$dropped" ]]; then + body+=$'\n' + body+=$'\n' + body+="The tools/ copy had content not present in the kernel sources," + body+=$'\n' + body+="dropped here after manual review:" + body+=$'\n' + body+=$'\n' + body+="$dropped" + fi + + body+=$'\n' + body+=$'\n' + body+="This addresses this perf build warning:" + body+=$'\n' + body+=$'\n' + body+=" Warning: Kernel ABI header differences:" + body+=$'\n' + body+=" diff -u $tools_path $kernel_path" + body+=$'\n' + body+=$'\n' + body+="Please see tools/include/uapi/README for further details." + + # Add Cc for kernel commit authors + local cc_lines="" + if [[ -n "$pickup_commits" ]]; then + cc_lines=$(format_commits 'Cc: %an <%ae>' "$pickup_commits" | sort -u) + fi + + body+=$'\n' + body+=$'\n' + if [[ -n "$cc_lines" ]]; then + body+="$cc_lines"$'\n' + fi + body+="$(sign_off)" + + cleanup_tmpdir "$beauty_tmpdir" + + if ! git -C "$KERNEL_DIR" add "$tools_path"; then + err "Failed to stage $tools_path" + COMMIT_FAILED+=("$tools_path") + return 1 + fi + if ! git -C "$KERNEL_DIR" commit "$tools_path" \ + -m "$(printf '%s\n\n%s\n' "$subject" "$body")"; then + err "Commit failed for $tools_path" + COMMIT_FAILED+=("$tools_path") + return 1 + fi + + SYNCED_OK+=("$tools_path") + info "Committed: $subject" + echo "" +} + +# --- Group related headers ----------------------------------------------- +# +# Some headers should be committed together (e.g. all KVM arch headers, +# all syscall tables). This function processes groups. + +process_group() { + local group_name="$1" + shift + local pairs=("$@") + + if [[ ${#pairs[@]} -eq 0 ]]; then + return + fi + + local file_count + file_count=$((${#pairs[@]} / 2)) + info "Processing group: $group_name ($file_count files)" + + local group_slug="${group_name// /-}" + + local all_kernel_commits="" + local all_cc_lines="" + local all_warnings="" + local group_beauty_tmpdir="" + local group_beauty_body="" + local group_beauty_scripts=0 + local group_dropped="" + local -a group_forced=() + local files_to_add=() + + # Collect info for all files in the group + local i=0 + while [[ $i -lt ${#pairs[@]} ]]; do + local tools_path="${pairs[$i]}" + local kernel_path="${pairs[$((i+1))]}" + local fidx=$i + i=$((i + 2)) + + if [[ -n "$HEADER_FILTER" ]] && [[ "$kernel_path" != *"$HEADER_FILTER"* ]]; then + continue + fi + + if has_ignore_rules "$kernel_path"; then + local sync_point pickup_commits="" merge_commits="" + sync_point=$(find_sync_point "$kernel_path" "$tools_path") + if [[ -n "$sync_point" ]]; then + pickup_commits=$(find_kernel_commits "$kernel_path" \ + "$tools_path" "$sync_point") + merge_commits=$(merge_pickup_commits "$kernel_path" \ + "$tools_path" "$sync_point") + fi + if [[ -n "$pickup_commits" || -n "$merge_commits" ]]; then + echo "" + echo " Kernel commits since last sync ($tools_path):" + if [[ -n "$pickup_commits" ]]; then + format_commits ' %h ("%s")' "$pickup_commits" + fi + if [[ -n "$merge_commits" ]]; then + format_commits ' %h ("%s")' "$merge_commits" + fi + local ccs + ccs=$(format_commits 'Cc: %an <%ae>' "$pickup_commits" | sort -u) + if [[ -n "$ccs" ]]; then + echo "" + echo "$ccs" | sed 's/^/ /' + fi + echo "" + fi + warn "$tools_path carries intentional edits, sync by hand" + HAND_SYNC+=("$tools_path") + continue + fi + + local sync_point direction + sync_point=$(find_sync_point "$kernel_path" "$tools_path") + direction=$(sync_direction "$kernel_path" "$tools_path" \ + "$sync_point") + case "$direction" in + delete) + warn "$tools_path is ahead of the kernel sources, leaving it alone" + TOOLS_AHEAD+=("$tools_path") + continue + ;; + both) + if merge_matches_kernel "$kernel_path" "$tools_path" "$sync_point"; then + detail "Tools/ changes are already in the kernel" + elif [[ $FORCE -eq 0 ]]; then + err "$tools_path and $kernel_path both have content the other lacks, needs manual investigation" + MANUAL+=("$tools_path") + continue + else + warn "Both copies have unique content, dropping the tools/ one (--force)" + local file_dropped + file_dropped=$(tools_only_lines "$tools_path" "$sync_point") + if [[ -n "$file_dropped" ]]; then + group_dropped+="$file_dropped"$'\n' + fi + group_forced+=("$tools_path") + fi + ;; + esac + + local pickup_commits="" + if [[ -n "$sync_point" ]]; then + pickup_commits=$(find_kernel_commits "$kernel_path" \ + "$tools_path" "$sync_point") + else + warn "No previous sync commit found for $tools_path" + fi + if [[ -n "$pickup_commits" ]]; then + all_kernel_commits+=$(format_commits ' %h ("%s")' "$pickup_commits") + all_kernel_commits+=$'\n' + + local ccs + ccs=$(format_commits 'Cc: %an <%ae>' "$pickup_commits") + if [[ -n "$ccs" ]]; then + all_cc_lines+="$ccs"$'\n' + fi + fi + + # A merge can bring content of its own; list it too, it is not the + # author of a change, so it gets no Cc. + local merge_commits="" + merge_commits=$(merge_pickup_commits "$kernel_path" "$tools_path" \ + "$sync_point") + if [[ -n "$merge_commits" ]]; then + all_kernel_commits+=$(format_commits ' %h ("%s")' "$merge_commits") + all_kernel_commits+=$'\n' + fi + + all_warnings+=" diff -u $tools_path $kernel_path"$'\n' + + # Beauty string tables; the script reads the tools/ copy, so this + # runs before the copy too, also in a dry run. + local -a group_script_list=() + while IFS= read -r script; do + [[ -z "$script" ]] && continue + group_script_list+=("$script") + done < <(find_beauty_scripts "$kernel_path" "$tools_path") + if [[ ${#group_script_list[@]} -gt 0 ]]; then + group_beauty_scripts=1 + [[ -z "$group_beauty_tmpdir" ]] && group_beauty_tmpdir=$(mktemp -d) + local sidx=0 + for script in "${group_script_list[@]}"; do + local script_name + script_name=$(basename "$script") + local before="$group_beauty_tmpdir/beauty.$fidx.$sidx.before" + local after="$group_beauty_tmpdir/beauty.$fidx.$sidx.after" + + if ! run_beauty_pass "$script" "$kernel_path" "$tools_path" \ + "$group_beauty_tmpdir" "$fidx.$sidx"; then + COPY_FAILED+=("$tools_path") + for f in "${files_to_add[@]}"; do + git -C "$KERNEL_DIR" checkout -- "$f" || \ + err "Failed to revert $f" + done + cleanup_tmpdir "$group_beauty_tmpdir" + return 1 + fi + + if ! diff -q "$before" "$after" > /dev/null 2>&1; then + echo "" + info "Beauty string changes from $script_name:" + diff -u "$before" "$after" | head -60 || true + group_beauty_body+=$(beauty_body_fragment "$script" \ + "$kernel_path" "$tools_path" "$before" "$after") + group_beauty_body+=$'\n' + fi + sidx=$((sidx + 1)) + done + fi + + if [[ $DRY_RUN -eq 0 ]]; then + if ! cp "$KERNEL_DIR/$kernel_path" "$KERNEL_DIR/$tools_path"; then + err "Failed to copy $kernel_path -> $tools_path" + COPY_FAILED+=("$tools_path") + for f in "${files_to_add[@]}"; do + git -C "$KERNEL_DIR" checkout -- "$f" || \ + err "Failed to revert $f" + done + cleanup_tmpdir "$group_beauty_tmpdir" + return 1 + fi + files_to_add+=("$tools_path") + info "Copied $kernel_path -> $tools_path" + else + info "(dry run) Would copy $kernel_path -> $tools_path" + fi + done + + # The beauty before/after files are captured into the fragments above. + cleanup_tmpdir "$group_beauty_tmpdir" + + if [[ ${#files_to_add[@]} -eq 0 ]]; then + FORCED+=("${group_forced[@]+"${group_forced[@]}"}") + return + fi + + if [[ $BUILD -eq 1 ]]; then + # Collect all unique build targets for the group + local group_targets="" + for f in "${files_to_add[@]}"; do + classify_header "$f" + group_targets+=$(find_build_targets "$f" "$HDR_CLASS") + group_targets+=$'\n' + done + group_targets=$(echo "$group_targets" | sort -u | grep -v '^$') + + show_build_targets "group $group_name" "$group_targets" + local build_failed=0 + while IFS=: read -r name dir; do + [[ -z "$name" ]] && continue + detail " Build-testing $name ($dir)..." + local log="$OUTPUT_DIR/$name.$group_slug.log" + if ! build_target "$name" "$dir" > "$log" 2>&1; then + err " $name build failed, see $log" + build_failed=1 + fi + done <<< "$group_targets" + + if [[ $build_failed -eq 1 ]]; then + err "Build failed after copying group $group_name — reverting." + for f in "${files_to_add[@]}"; do + git -C "$KERNEL_DIR" checkout -- "$f" || \ + err "Failed to revert $f" + BUILD_FAILED+=("$f (group: $group_name)") + done + return 1 + fi + info "Build OK." + fi + + FORCED+=("${group_forced[@]+"${group_forced[@]}"}") + + if [[ $DRY_RUN -eq 1 ]]; then + return + fi + + if [[ $COMMIT -eq 0 ]]; then + for f in "${files_to_add[@]}"; do + SYNCED_OK+=("$f") + done + return + fi + + # Deduplicate kernel commits and Cc lines; the commits keep the git log + # order they came in, the Cc lines stay sorted. + all_kernel_commits=$(printf '%s\n' "$all_kernel_commits" | \ + awk 'NF && !seen[$0]++') + all_cc_lines=$(echo "$all_cc_lines" | sort -u | grep -v '^$' || true) + + local subject="tools headers: Sync $group_name with the kernel sources" + local body="" + + if [[ -n "$all_kernel_commits" ]]; then + body+="To pick up the changes in:" + body+=$'\n' + body+=$'\n' + body+="$all_kernel_commits" + body+=$'\n' + fi + + if [[ -n "$group_beauty_body" ]]; then + body+=$'\n' + body+="That introduce these new entries in the string tables:" + body+=$'\n' + body+="$group_beauty_body" + elif [[ $group_beauty_scripts -eq 1 ]]; then + body+=$'\n' + body+="That don't result in changes to the string tables generated" + body+=" from these headers." + fi + + if [[ -n "$group_dropped" ]]; then + body+=$'\n' + body+=$'\n' + body+="The tools/ copies had content not present in the kernel" + body+=" sources, dropped here after manual review:" + body+=$'\n' + body+=$'\n' + body+="${group_dropped%$'\n'}" + fi + + body+=$'\n' + body+=$'\n' + body+="This addresses these perf build warnings:" + body+=$'\n' + body+=$'\n' + body+=" Warning: Kernel ABI header differences:" + body+=$'\n' + body+="$all_warnings" + body+=$'\n' + body+="Please see tools/include/uapi/README for further details." + body+=$'\n' + body+=$'\n' + if [[ -n "$all_cc_lines" ]]; then + body+="$all_cc_lines"$'\n' + fi + body+="$(sign_off)" + + for f in "${files_to_add[@]}"; do + if ! git -C "$KERNEL_DIR" add "$f"; then + err "Failed to stage $f" + for g in "${files_to_add[@]}"; do + COMMIT_FAILED+=("$g") + done + return 1 + fi + done + + if ! git -C "$KERNEL_DIR" commit "${files_to_add[@]}" \ + -m "$(printf '%s\n\n%s\n' "$subject" "$body")"; then + err "Commit failed for group $group_name" + for f in "${files_to_add[@]}"; do + COMMIT_FAILED+=("$f") + done + return 1 + fi + + for f in "${files_to_add[@]}"; do + SYNCED_OK+=("$f") + done + + info "Committed: $subject" + echo "" +} + +# --- Main ----------------------------------------------------------------- + +main() { + cd "$KERNEL_DIR" + + if [[ ! -d include/uapi ]]; then + err "Must be run from a kernel tree root (or tools/perf/)" + exit 1 + fi + + if [[ ! -d tools/perf ]]; then + err "Cannot find tools/perf/ in kernel tree" + exit 1 + fi + + setup_beauty_args || true + + if [[ $BUILD -eq 1 ]]; then + # umask covers every level created, -m with -p only the last. + (umask 077 && mkdir -p "$OUTPUT_DIR") + fi + + # Check for uncommitted changes in tools/ + if ! git diff --quiet -- tools/include/ tools/perf/trace/beauty/ tools/perf/util/ tools/perf/arch/*/entry/syscalls/ tools/scripts/ tools/lib/ tools/arch/; then + warn "You have uncommitted changes in tools/. Commit or stash them first." + if [[ $DRY_RUN -eq 0 ]]; then + exit 1 + fi + fi + + info "Running check-headers to detect stale copies..." + echo "" + + local stale + if ! stale=$(detect_stale_headers); then + info "All headers are up to date." + exit 0 + fi + + echo "Stale headers found:" + echo "$stale" + echo "" + + # Parse the stale header pairs and group them + # Each line is: tools_path kernel_path + local kvm_group=() + local syscall_group=() + local singles=() + + while IFS=' ' read -r tools_path kernel_path; do + case "$kernel_path" in + *kvm.h|*svm.h|*vmx.h|*sie.h) + kvm_group+=("$tools_path" "$kernel_path") + ;; + *syscall*.tbl|*unistd.h) + syscall_group+=("$tools_path" "$kernel_path") + ;; + *) + singles+=("$tools_path" "$kernel_path") + ;; + esac + done <<< "$stale" + + # Process individual headers first (beauty and simple copies). + # || true: continue on failure; failures are tracked in BUILD_FAILED[], + # COPY_FAILED[] and COMMIT_FAILED[] and reported in the summary. + local i=0 + while [[ $i -lt ${#singles[@]} ]]; do + process_header "${singles[$i]}" "${singles[$((i+1))]}" || true + i=$((i + 2)) + done + + # Process grouped headers + if [[ ${#kvm_group[@]} -gt 0 ]]; then + process_group "KVM headers" "${kvm_group[@]}" || true + fi + + if [[ ${#syscall_group[@]} -gt 0 ]]; then + process_group "syscall tables and unistd.h" "${syscall_group[@]}" || true + fi + + # Summary + echo "" + echo "=========================================" + echo " Summary" + echo "=========================================" + + if [[ ${#SYNCED_OK[@]} -gt 0 ]]; then + info "Synced successfully (${#SYNCED_OK[@]}):" + for f in "${SYNCED_OK[@]}"; do + echo " $f" + done + fi + + if [[ ${#BUILD_FAILED[@]} -gt 0 ]]; then + echo "" + err "Build failures (${#BUILD_FAILED[@]}) — reverted, need manual attention:" + for f in "${BUILD_FAILED[@]}"; do + echo " $f" + done + fi + + if [[ ${#COPY_FAILED[@]} -gt 0 ]]; then + echo "" + err "Copy failures (${#COPY_FAILED[@]}) — need manual attention:" + for f in "${COPY_FAILED[@]}"; do + echo " $f" + done + fi + + if [[ ${#COMMIT_FAILED[@]} -gt 0 ]]; then + echo "" + err "Commit failures (${#COMMIT_FAILED[@]}) — need manual attention:" + for f in "${COMMIT_FAILED[@]}"; do + echo " $f" + done + fi + + if [[ ${#TOOLS_AHEAD[@]} -gt 0 ]]; then + echo "" + warn "tools/ copies ahead of the kernel, left alone (${#TOOLS_AHEAD[@]}):" + for f in "${TOOLS_AHEAD[@]}"; do + echo " $f" + done + fi + + if [[ ${#HAND_SYNC[@]} -gt 0 ]]; then + echo "" + warn "Carry intentional edits, need manual sync (${#HAND_SYNC[@]}):" + for f in "${HAND_SYNC[@]}"; do + echo " $f" + done + fi + + if [[ ${#MANUAL[@]} -gt 0 ]]; then + echo "" + err "Left for manual investigation (${#MANUAL[@]}):" + for f in "${MANUAL[@]}"; do + echo " $f" + done + fi + + if [[ ${#FORCED[@]} -gt 0 ]]; then + echo "" + info "Synced with --force, tools/ additions dropped (${#FORCED[@]}):" + for f in "${FORCED[@]}"; do + echo " $f" + done + fi + + # Final check-headers run to see what's still out of sync + echo "" + local remaining + if remaining=$(detect_stale_headers); then + warn "Headers still out of sync:" + echo "$remaining" + else + info "All headers are now in sync." + fi + + if [[ ${#BUILD_FAILED[@]} -gt 0 || ${#COPY_FAILED[@]} -gt 0 || \ + ${#COMMIT_FAILED[@]} -gt 0 || ${#MANUAL[@]} -gt 0 ]]; then + exit 1 + fi +} + +main "$@" |
