#!/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"