blob: 01e2d108432f0116412f61a7ed5e7d8d0fb91754 [file]
#!/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 "$@"