summaryrefslogtreecommitdiff
path: root/tools/perf/scripts/sync-headers.sh
diff options
context:
space:
mode:
Diffstat (limited to 'tools/perf/scripts/sync-headers.sh')
-rwxr-xr-xtools/perf/scripts/sync-headers.sh1676
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 "$@"