Files
ARSV/scripts/rpmsetinit/compare_sisyphus_set_versions.sh
T
2026-07-20 21:58:04 +03:00

612 lines
21 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
REPO_ROOT=$(cd -- "$SCRIPT_DIR/.." && pwd)
NEWSET_C="$REPO_ROOT/reimplement/newset.c"
NEWSET_COMPAT="$SCRIPT_DIR/newset_compat.h"
NEWSET_WRAPPER="$SCRIPT_DIR/newset_mkset.c"
PACKAGE_PARSER="$SCRIPT_DIR/parse_sisyphus_packages.awk"
RESUME_PARSER="$SCRIPT_DIR/completed_set_versions.awk"
PAYLOAD_HELPERS="$SCRIPT_DIR/rpm_payload_safety.sh"
# shellcheck source=scripts/rpm_payload_safety.sh
source "$PAYLOAD_HELPERS"
MIRROR=https://ftp.altlinux.org/pub/distributions/ALTLinux
REPORT=sisyphus-set-compare.tsv
LIMIT=
ALL=0
RESUME=0
KEEP_WORK=0
BUILD_MKSET=
PACKAGES=()
usage()
{
cat <<'EOF'
Usage:
compare_sisyphus_set_versions.sh --all [OPTIONS]
compare_sisyphus_set_versions.sh --limit N [OPTIONS]
compare_sisyphus_set_versions.sh --package NAME [--package NAME ...] [OPTIONS]
compare_sisyphus_set_versions.sh --build-mkset PATH
Scopes:
--all Process every x86_64/noarch package record.
--limit N Process only the first N records (testing).
--package NAME Process one named package; may be repeated (testing).
Options:
--report FILE Streaming TSV report (default: sisyphus-set-compare.tsv).
--resume Skip completed package/architecture/version records.
--keep-work Preserve the temporary working directory.
--mirror URL ALT repository root.
--build-mkset PATH Build only the mkset-compatible newset.c wrapper and exit.
-h, --help Show this help.
The report gets a START row before processing and DEPENDENCY/SUMMARY rows are
appended after every package, so it can be monitored while the script runs.
No repository-wide run is possible without the explicit --all option.
EOF
}
die()
{
printf 'error: %s\n' "$*" >&2
exit 2
}
while (($#)); do
case "$1" in
--all) ALL=1; shift ;;
--limit) (($# >= 2)) || die '--limit requires N'; LIMIT=$2; shift 2 ;;
--package) (($# >= 2)) || die '--package requires NAME'; PACKAGES+=("$2"); shift 2 ;;
--report) (($# >= 2)) || die '--report requires FILE'; REPORT=$2; shift 2 ;;
--resume) RESUME=1; shift ;;
--keep-work) KEEP_WORK=1; shift ;;
--mirror) (($# >= 2)) || die '--mirror requires URL'; MIRROR=${2%/}; shift 2 ;;
--build-mkset) (($# >= 2)) || die '--build-mkset requires PATH'; BUILD_MKSET=$2; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) die "unknown option: $1" ;;
esac
done
build_mkset()
(
{
local output=$1 build
build=$(mktemp -d "${TMPDIR:-/tmp}/arsv-newset-build.XXXXXX")
trap 'rm -rf "$build"' EXIT
: >"$build/rpmlib.h"
: >"$build/system.h"
mkdir -p "$(dirname -- "$output")"
"${CC:-cc}" -O2 -std=gnu11 -D_GNU_SOURCE \
-I"$build" -include "$NEWSET_COMPAT" \
-c "$NEWSET_C" -o "$build/newset.o"
"${CC:-cc}" -O2 -std=gnu11 -D_GNU_SOURCE \
"$NEWSET_WRAPPER" "$build/newset.o" -o "$output"
chmod 755 "$output"
}
)
if [[ -n $BUILD_MKSET ]]; then
build_mkset "$BUILD_MKSET"
exit 0
fi
[[ $MIRROR == http://* || $MIRROR == https://* ]] || die '--mirror must use http:// or https://'
[[ $MIRROR != *[$'\t\r\n ']* ]] || die '--mirror must not contain whitespace'
for package in "${PACKAGES[@]}"; do
[[ $package =~ ^[A-Za-z0-9][A-Za-z0-9+_.-]*$ ]] || die "invalid package name: $package"
done
scope_count=$ALL
[[ -n $LIMIT ]] && scope_count=$((scope_count + 1))
((${#PACKAGES[@]} > 0)) && scope_count=$((scope_count + 1))
((scope_count == 1)) || die 'use --all, --limit, or --package (exactly one scope)'
if [[ -n $LIMIT && ! $LIMIT =~ ^[1-9][0-9]*$ ]]; then
die '--limit must be a positive integer'
fi
for command in apt-get apt-cache rpm rpmquery rpm2cpio cpio curl md5sum awk sed sort find cp cc realpath; do
command -v "$command" >/dev/null || die "required command not found: $command"
done
rpmlibdir=$(rpm --eval '%_rpmlibdir')
[[ -x $rpmlibdir/find-provides ]] || die "$rpmlibdir/find-provides is missing (install rpm-build)"
[[ -x $rpmlibdir/find-requires ]] || die "$rpmlibdir/find-requires is missing (install rpm-build)"
case $REPORT in
/*) ;;
*) REPORT="$PWD/$REPORT" ;;
esac
mkdir -p "$(dirname -- "$REPORT")"
if [[ ! -s $REPORT ]]; then
printf 'timestamp\trecord\tpackage\tarchitecture\tstatus\tside\tcapability\toperator\texpected\tgenerated\tdetail\n' >"$REPORT"
fi
clean_field()
{
local value=${1-}
value=${value//$'\t'/ }
value=${value//$'\r'/ }
value=${value//$'\n'/ }
printf '%s' "$value"
}
report_row()
{
local fields=() field row
(($# == 11)) || {
printf 'internal error: report row has %s fields, expected 11\n' "$#" >&2
return 1
}
for field in "$@"; do
fields+=("$(clean_field "$field")")
done
if [[ ${fields[1]} == SUMMARY ]]; then
case ${fields[10]} in
'') fields[10]='complete=1' ;;
*'; ') fields[10]="${fields[10]}complete=1" ;;
*) fields[10]="${fields[10]}; complete=1" ;;
esac
fi
row=${fields[0]}
for field in "${fields[@]:1}"; do
printf -v row '%s\t%s' "$row" "$field"
done
printf '%s\n' "$row" >>"$REPORT"
}
timestamp()
{
date -u '+%Y-%m-%dT%H:%M:%SZ'
}
WORK=$(mktemp -d "${TMPDIR:-/tmp}/arsv-sisyphus-sets.XXXXXX")
cleanup()
{
if ((KEEP_WORK)); then
printf 'work directory preserved: %s\n' "$WORK" >&2
else
rm -rf "$WORK"
fi
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
mkdir -p "$WORK/apt/lists/partial" "$WORK/apt/archives/partial" "$WORK/tools"
: >"$WORK/apt/apt.conf"
: >"$WORK/apt/status"
printf '%s\n' \
"rpm [alt] $MIRROR Sisyphus/x86_64 classic" \
"rpm [alt] $MIRROR Sisyphus/noarch classic" >"$WORK/apt/sources.list"
APT_OPTIONS=(
-o 'Dir::Etc::main=-'
-o 'Dir::Etc::parts=-'
-o "Dir::Etc::sourcelist=$WORK/apt/sources.list"
-o 'Dir::Etc::sourceparts=-'
-o 'Dir::Etc::preferences=-'
-o 'Dir::Etc::preferencesparts=-'
-o "Dir::State::lists=$WORK/apt/lists"
-o "Dir::State::status=$WORK/apt/status"
-o "Dir::Cache::archives=$WORK/apt/archives"
-o "Dir::Cache::pkgcache=$WORK/apt/pkgcache.bin"
-o "Dir::Cache::srcpkgcache=$WORK/apt/srcpkgcache.bin"
)
apt_get()
{
APT_CONFIG="$WORK/apt/apt.conf" apt-get "${APT_OPTIONS[@]}" "$@"
}
apt_cache()
{
APT_CONFIG="$WORK/apt/apt.conf" apt-cache "${APT_OPTIONS[@]}" "$@"
}
NEW_MKSET="$WORK/new-mkset"
build_mkset "$NEW_MKSET"
cp -as "$rpmlibdir"/. "$WORK/tools/"
rm -f "$WORK/tools/mkset"
ln -s "$NEW_MKSET" "$WORK/tools/mkset"
report_row "$(timestamp)" RUN - - start - - - - - "updating isolated Sisyphus indexes"
if ! apt_get update >"$WORK/apt-update.log" 2>&1; then
detail=$(sed -n '/./{p;q;}' "$WORK/apt-update.log")
report_row "$(timestamp)" RUN - - apt_update_error - - - - - "$detail"
exit 1
fi
QUEUE="$WORK/packages.tsv"
if ! apt_cache dumpavail |
awk -f "$PACKAGE_PARSER" |
LC_ALL=C sort -t$'\034' -k1,1 -k2,2 >"$QUEUE"; then
report_row "$(timestamp)" RUN - - metadata_error - - - - - 'unable to parse apt-cache dumpavail'
exit 1
fi
SELECTED="$WORK/selected.tsv"
if ((${#PACKAGES[@]})); then
: >"$SELECTED"
for package in "${PACKAGES[@]}"; do
if ! awk -F '\034' -v package="$package" '$1 == package { print; found=1 } END { exit !found }' \
"$QUEUE" >>"$SELECTED"; then
die "package not found in Sisyphus x86_64/noarch: $package"
fi
done
elif [[ -n $LIMIT ]]; then
sed -n "1,${LIMIT}p" "$QUEUE" >"$SELECTED"
else
cp "$QUEUE" "$SELECTED"
fi
selected_count=$(wc -l <"$SELECTED")
report_row "$(timestamp)" RUN - - ready - - - - - "selected packages: $selected_count"
printf 'selected packages: %s; report: %s\n' "$selected_count" "$REPORT"
declare -A COMPLETED=()
if ((RESUME)); then
while IFS=$'\t' read -r package architecture version; do
COMPLETED["$package"$'\t'"$architecture"$'\t'"$version"]=1
done < <(awk -f "$RESUME_PARSER" "$REPORT")
fi
first_log_line()
{
local file=$1
[[ -s $file ]] || return 0
sed -n '/./{p;q;}' "$file" | cut -c1-500
}
last_log_line()
{
local file=$1
[[ -s $file ]] || return 0
awk 'NF { line=$0 } END { print line }' "$file" | cut -c1-500
}
extract_rpm()
{
local package_file=$1 root=$2 log=$3 mode=$4
local metadata="$root/../.arsv-file-metadata"
local patterns="$root/../.arsv-nonlink-patterns"
local symlinks="$root/../.arsv-symlinks"
local field_separator=$'\034' record_separator=$'\035' record
local filename link_target relative archive_name
local query_format="[%{FILENAMES}${field_separator}%{FILELINKTOS}${record_separator}]"
if ! rpmquery -p --qf "$query_format" \
"$package_file" >"$metadata" 2>>"$log"; then
return 1
fi
case $mode in
symlinks) : >"$symlinks" ;;
files) : >"$patterns" ;;
*) printf 'internal error: invalid RPM extraction mode: %s\n' "$mode" >>"$log"; return 1 ;;
esac
while IFS= read -r -d "$record_separator" record; do
if ! rpm_split_file_record "$record"; then
printf 'unsupported RPM filename or symlink record\n' >>"$log"
return 1
fi
filename=$RPM_FILENAME
link_target=$RPM_LINK_TARGET
[[ -n $filename && $filename == /* ]] || {
printf 'unsafe RPM filename: %q\n' "$filename" >>"$log"
return 1
}
relative=${filename#/}
if [[ $relative == '..' || $relative == ../* || $relative == */../* || $relative == */.. ]]; then
printf 'unsafe RPM filename: %q\n' "$filename" >>"$log"
return 1
fi
if [[ -n $link_target && $link_target != '(none)' ]]; then
if [[ $mode == symlinks ]]; then
printf '%s\t%s\n' "$relative" "$link_target" >>"$symlinks"
fi
elif [[ $mode == files ]]; then
archive_name=./$relative
if ! rpm_cpio_literal_pattern "$archive_name" >>"$patterns"; then
printf 'unsupported RPM filename: %q\n' "$filename" >>"$log"
return 1
fi
fi
done <"$metadata"
if [[ $mode == files ]]; then
if ! rpm2cpio "$package_file" |
cpio -it --quiet --no-absolute-filenames 2>>"$log" |
while IFS= read -r member; do
case $member in
/*|../*|*/../*|*/..)
printf 'unsafe cpio member: %q\n' "$member" >>"$log"
exit 1
;;
esac
done; then
return 1
fi
if [[ -s $patterns ]]; then
if ! rpm2cpio "$package_file" |
(cd "$root" && cpio -idm --quiet --no-absolute-filenames -E "$patterns") \
2>>"$log"; then
return 1
fi
fi
return 0
fi
cat "$symlinks"
}
normalize_expected()
{
local side=$1 package_file=$2 output=$3
if [[ $side == provides ]]; then
rpmquery -p --qf \
'[%{PROVIDENAME}\t%{PROVIDEFLAGS:depflags}\t%{PROVIDEVERSION}\n]' \
"$package_file"
else
rpmquery -p --qf \
'[%{REQUIRENAME}\t%{REQUIREFLAGS:depflags}\t%{REQUIREVERSION}\n]' \
"$package_file"
fi | awk -F '\t' '$3 ~ /^set:/ { print $1 "\t" $2 "\t" $3 }' |
LC_ALL=C sort -u >"$output"
}
normalize_generated()
{
local side=$1 input=$2 output=$3
if [[ $side == provides ]]; then
awk '$2 == "=" && $3 ~ /^set:/ { print $1 "\t" $2 "\t" $3 }' "$input"
else
awk '$2 == ">=" && $3 ~ /^set:/ { print $1 "\t" $2 "\t" $3 }' "$input"
fi | LC_ALL=C sort -u >"$output"
}
compare_side()
{
local side=$1 package=$2 architecture=$3 expected=$4 generated=$5 output=$6
if ! awk -F '\t' -v OFS='\034' '
FILENAME == ARGV[1] { key=$1 "\034" $2; expected[key]=$3; keys[key]=1; next }
{ key=$1 "\034" $2; generated[key]=$3; keys[key]=1 }
END {
for (key in keys) {
split(key, parts, "\034")
e=expected[key]; g=generated[key]
if (e == "") status="extra_generated"
else if (g == "") status="missing_generated"
else if (e == g) status="match"
else status="mismatch"
print status, parts[1], parts[2], e, g
}
}
' "$expected" "$generated" |
LC_ALL=C sort -t$'\034' -k2,2 -k3,3 >"$output"; then
return 1
fi
# A non-whitespace separator preserves empty expected/generated fields.
while IFS=$'\034' read -r status capability operator expected_set generated_set; do
[[ -n $status ]] || continue
report_row "$(timestamp)" DEPENDENCY "$package" "$architecture" "$status" \
"$side" "$capability" "$operator" "$expected_set" "$generated_set" -
done <"$output"
}
process_package()
{
local package=$1 architecture=$2 version=$3 filename=$4 md5=$5
local has_provides=$6 has_requires=$7
local key=$package$'\t'$architecture$'\t'$version
if [[ -n ${COMPLETED[$key]-} ]]; then
printf 'skip completed: %s.%s\n' "$package" "$architecture"
return 0
fi
report_row "$(timestamp)" START "$package" "$architecture" processing - - - - - "$version"
printf 'process: %s.%s\n' "$package" "$architecture"
if ((has_provides == 0 && has_requires == 0)); then
report_row "$(timestamp)" SUMMARY "$package" "$architecture" no_set_metadata \
- - - - - 'APT metadata contains no set: Provides/Requires'
return 0
fi
local package_work="$WORK/package"
rm -rf "$package_work"
mkdir -p "$package_work/root" "$package_work/archives/partial"
local target="$package_work/target.rpm"
local url="$MIRROR/Sisyphus/files/$architecture/RPMS/$filename"
local log="$package_work/package.log"
if ! curl --silent --show-error -fL --retry 3 --retry-delay 2 \
-o "$target" "$url" >"$log" 2>&1; then
report_row "$(timestamp)" SUMMARY "$package" "$architecture" download_error \
- - - - - "$(first_log_line "$log")"
return 0
fi
if [[ -n $md5 ]]; then
local actual_md5
if ! actual_md5=$(md5sum "$target" | awk '{print $1}'); then
report_row "$(timestamp)" SUMMARY "$package" "$architecture" checksum_error \
- - - "$md5" - "$filename"
return 0
fi
if [[ $actual_md5 != "$md5" ]]; then
report_row "$(timestamp)" SUMMARY "$package" "$architecture" checksum_error \
- - - "$md5" "$actual_md5" "$filename"
return 0
fi
fi
local -a payload_rpms=("$target")
if ((has_requires)); then
mkdir -p "$package_work/root/var/lib/rpm"
if ! rpm --root "$package_work/root" --initdb >>"$log" 2>&1; then
report_row "$(timestamp)" SUMMARY "$package" "$architecture" rpmdb_error \
- - - - - "$(last_log_line "$log")"
return 0
fi
if ! apt_get -y -d \
-o "Dir::Cache::archives=$package_work/archives" \
-o "RPM::RootDir=$package_work/root" \
install "$target" >>"$log" 2>&1; then
report_row "$(timestamp)" SUMMARY "$package" "$architecture" dependency_download_error \
- - - - - "$(last_log_line "$log")"
return 0
fi
local dependency_rpm
for dependency_rpm in "$package_work"/archives/*.rpm; do
[[ -f $dependency_rpm ]] || continue
payload_rpms+=("$dependency_rpm")
done
fi
local payload_rpm extraction_status
local symlink_manifest="$package_work/symlinks.tsv"
: >"$symlink_manifest"
for payload_rpm in "${payload_rpms[@]}"; do
if ! extract_rpm "$payload_rpm" "$package_work/root" "$log" symlinks \
>>"$symlink_manifest"; then
if [[ $payload_rpm == "$target" ]]; then
extraction_status=extract_error
else
extraction_status=dependency_extract_error
fi
report_row "$(timestamp)" SUMMARY "$package" "$architecture" "$extraction_status" \
- - - - - "$(last_log_line "$log")"
return 0
fi
done
if ! rpm_install_symlink_manifest "$package_work/root" "$symlink_manifest" "$log"; then
if ((has_requires)); then
extraction_status=dependency_extract_error
else
extraction_status=extract_error
fi
report_row "$(timestamp)" SUMMARY "$package" "$architecture" "$extraction_status" \
- - - - - "$(last_log_line "$log")"
return 0
fi
for payload_rpm in "${payload_rpms[@]}"; do
if ! extract_rpm "$payload_rpm" "$package_work/root" "$log" files; then
if [[ $payload_rpm == "$target" ]]; then
extraction_status=extract_error
else
extraction_status=dependency_extract_error
fi
report_row "$(timestamp)" SUMMARY "$package" "$architecture" "$extraction_status" \
- - - - - "$(last_log_line "$log")"
return 0
fi
done
if ! rpmquery -p --qf '[%{FILENAMES}\n]' "$target" |
awk -v root="$package_work/root" \
'{ if (substr($0,1,1)=="/") print root $0; else print root "/" $0 }' |
while IFS= read -r path; do
if [[ -e $path || -L $path ]]; then
printf '%s\n' "$path"
fi
done >"$package_work/files"; then
report_row "$(timestamp)" SUMMARY "$package" "$architecture" file_list_error \
- - - - - 'rpmquery failed while reading package filenames'
return 0
fi
local scanner_env=(
"RPM_BUILD_ROOT=$package_work/root"
"RPM_SUBPACKAGE_NAME=$package"
"RPM_PACKAGE_NAME=$package"
"RPMB_TOOLS_DIR=$WORK/tools"
"RPMB_LIB_DIR=$rpmlibdir"
"RPMB_AUTODEPS_DIR=$rpmlibdir"
'RPM_FINDPROV_METHOD=none,lib'
'RPM_FINDREQ_METHOD=none,lib'
'RPM_SCRIPTS_DEBUG=0'
)
local side generator expected generated comparison
local total_mismatches=0 detail=''
for side in provides requires; do
[[ $side == provides && $has_provides == 1 ]] ||
[[ $side == requires && $has_requires == 1 ]] || continue
generator="$package_work/generated.$side.raw"
expected="$package_work/expected.$side.tsv"
generated="$package_work/generated.$side.tsv"
comparison="$package_work/comparison.$side.tsv"
if ! normalize_expected "$side" "$target" "$expected"; then
report_row "$(timestamp)" SUMMARY "$package" "$architecture" metadata_error \
"$side" - - - - 'rpmquery failed while reading set dependencies'
return 0
fi
if [[ $side == provides ]]; then
if ! env "${scanner_env[@]}" "$rpmlibdir/find-provides" \
<"$package_work/files" >"$generator" 2>"$package_work/$side.log"; then
report_row "$(timestamp)" SUMMARY "$package" "$architecture" generator_error \
"$side" - - - - "$(first_log_line "$package_work/$side.log")"
return 0
fi
else
if ! env "${scanner_env[@]}" "$rpmlibdir/find-requires" \
<"$package_work/files" >"$generator" 2>"$package_work/$side.log"; then
report_row "$(timestamp)" SUMMARY "$package" "$architecture" generator_error \
"$side" - - - - "$(first_log_line "$package_work/$side.log")"
return 0
fi
fi
if ! normalize_generated "$side" "$generator" "$generated"; then
report_row "$(timestamp)" SUMMARY "$package" "$architecture" generator_output_error \
"$side" - - - - 'unable to normalize generator output'
return 0
fi
if ! compare_side "$side" "$package" "$architecture" "$expected" "$generated" "$comparison"; then
report_row "$(timestamp)" SUMMARY "$package" "$architecture" comparison_error \
"$side" - - - - 'unable to compare generated dependencies'
return 0
fi
local expected_count generated_count mismatch_count extra_count
expected_count=$(wc -l <"$expected")
generated_count=$(wc -l <"$generated")
mismatch_count=$(awk -F '\034' \
'$1 == "mismatch" || $1 == "missing_generated" { count++ } END { print count+0 }' \
"$comparison")
extra_count=$(awk -F '\034' '$1 == "extra_generated" { count++ } END { print count+0 }' \
"$comparison")
total_mismatches=$((total_mismatches + mismatch_count + extra_count))
detail+="$side expected=$expected_count generated=$generated_count differences=$mismatch_count extras=$extra_count; "
done
local status=match
((total_mismatches == 0)) || status=mismatch
report_row "$(timestamp)" SUMMARY "$package" "$architecture" "$status" - - - - - "$detail"
sync -f "$REPORT" 2>/dev/null || true
}
while IFS=$'\034' read -r package architecture version filename md5 has_provides has_requires extra; do
if [[ -n ${extra-} || -z $package || -z $architecture || -z $version || -z $filename ||
! $has_provides =~ ^[01]$ || ! $has_requires =~ ^[01]$ ]]; then
report_row "$(timestamp)" RUN "$package" "$architecture" queue_error - - - - - \
'invalid package queue record'
continue
fi
process_package "$package" "$architecture" "$version" "$filename" "$md5" \
"$has_provides" "$has_requires"
done <"$SELECTED"
report_row "$(timestamp)" RUN - - complete - - - - - "processed selection: $selected_count"
printf 'complete; report: %s\n' "$REPORT"