This commit is contained in:
2026-07-20 00:56:41 +03:00
parent d906e14afe
commit 2bf8c8ede3
10 changed files with 1362 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
# Сравнение set-зависимостей Sisyphus с `newset.c`
Скрипт [`scripts/compare_sisyphus_set_versions.sh`](../scripts/compare_sisyphus_set_versions.sh)
сравнивает set-строки из RPM-заголовка с результатом `reimplement/newset.c`.
## Что именно сравнивается
1. Из изолированных APT-индексов `Sisyphus/x86_64` и `Sisyphus/noarch`
берутся `Package`, `Architecture`, `Version`, `Filename`, `MD5Sum`,
`Provides` и `Depends`/`Pre-Depends`.
2. RPM скачивается непосредственно из `Sisyphus/files/<arch>/RPMS` и
проверяется по `MD5Sum` из индекса.
3. Эталонные зависимости читаются из RPM-заголовка через массивы тегов:
`PROVIDENAME/PROVIDEFLAGS/PROVIDEVERSION` и
`REQUIRENAME/REQUIREFLAGS/REQUIREVERSION`.
4. Для воспроизведения зависимостей вызываются установленные вместе с
`rpm-build` программы `%_rpmlibdir/find-provides` и
`%_rpmlibdir/find-requires` с методом `none,lib`.
5. В каталоге инструментов заменяется **только** `mkset`: вместо штатного
`mkset` подставляется совместимая обвязка над `newset.c`. `provided_symbols`,
`ldd --bindings`, `eu-readelf`, `objdump`, `file` и остальные средства
отбора/извлечения символов остаются штатными инструментами `rpm-build`.
Для `Requires` APT получает точный уже скачанный RPM-файл как вход и скачивает
его полный runtime dependency closure в пустой пользовательский RPM root. Так
версия и архитектура целевого пакета не выбираются повторно по имени. APT
запускается с пустым `APT_CONFIG`; source list, package cache, archives, lists,
status и preferences перенаправлены во временный каталог, поэтому настройки,
hooks и RPM-база основной системы не участвуют в разрешении зависимостей.
Перед распаковкой проверяются имена элементов cpio и цели символических ссылок.
Абсолютная RPM-ссылка считается путём внутри виртуального package root: например,
`/usr/sbin/update-alternatives -> /bin/true` безопасно переписывается в
`../../bin/true`, а не направляется в `/bin/true` основной системы. Все symlink
сначала собираются из RPM-заголовков и создаются в порядке от родительских путей
к дочерним; затем `cpio` извлекает только нессылочные элементы. Благодаря этому
usrmerge-ссылки вроде `/bin -> usr/bin` создаются до файлов `/bin/*`.
Имена элементов с выходом через `..` и относительные symlink, фактически
выходящие из временного root, по-прежнему отклоняются. После проверки RPM-файлы
распаковываются без установки. Это позволяет штатному `rpm-build/ldd`
использовать интерпретатор и библиотеки из того же снимка Sisyphus, не изменяя
систему.
## Ограниченный тест
На ALT p11 с установленным `rpm-build`:
```sh
bash scripts/compare_sisyphus_set_versions.sh \
--package strace \
--report strace-set-report.tsv
```
Несколько пакетов:
```sh
bash scripts/compare_sisyphus_set_versions.sh \
--package strace \
--package libdw \
--report sample-set-report.tsv
```
Либо первые десять записей индекса:
```sh
bash scripts/compare_sisyphus_set_versions.sh \
--limit 10 \
--report sample-set-report.tsv
```
Без `--all`, `--limit` или `--package` скрипт завершится до обращения к
репозиторию. Это защищает от случайного полного запуска.
## Полный запуск
Команда предусмотрена, но в ходе разработки не запускалась:
```sh
bash scripts/compare_sisyphus_set_versions.sh \
--all \
--report sisyphus-set-report.tsv
```
Продолжение после прерывания:
```sh
bash scripts/compare_sisyphus_set_versions.sh \
--all \
--resume \
--report sisyphus-set-report.tsv
```
`--resume` пропускает только уже завершённую комбинацию
`package/architecture/version`; обновившийся пакет будет обработан заново.
Завершённость подтверждается маркером `complete=1` в полной 11-польной строке
`SUMMARY`, поэтому оборванная при записи строка не считается результатом.
Во время работы отчёт можно наблюдать отдельно:
```sh
tail -f sisyphus-set-report.tsv
```
`START` записывается до обработки пакета, а `DEPENDENCY` и `SUMMARY` — сразу
после сравнения пакета. Поэтому файл обновляется в процессе, а не только после
завершения всего обхода.
## Статусы отчёта
- `match` — set-строка из RPM совпала с результатом `newset.c`;
- `mismatch` — capability и оператор совпали, set-строки различаются;
- `missing_generated` — RPM содержит set-зависимость, но повторный запуск
`rpm-build` её не сгенерировал;
- `extra_generated` — повторный запуск сгенерировал зависимость, которой нет в
RPM-заголовке;
- `no_set_metadata` — в APT metadata пакета нет set-строк;
- `*_error` — ошибка скачивания, контрольной суммы, распаковки либо генератора.
`extra_generated` показывается в строках `DEPENDENCY` и делает итоговый статус
пакета равным `mismatch`. При анализе нужно учитывать, что бинарный RPM не
содержит исходных spec-фильтров (`filter_from_requires` и аналогичных), поэтому
часть таких расхождений может относиться не к `newset.c`, а к невозможности
полностью воспроизвести фильтрацию исходного spec. Их число отдельно указано в
поле `extras=N` строки `SUMMARY`.
+4
View File
@@ -1,3 +1,7 @@
# alt-rpm-set-version # alt-rpm-set-version
Research on ALT Linux RPM set.c and dependency comparison using encoded symbol sets. Research on ALT Linux RPM set.c and dependency comparison using encoded symbol sets.
## Tools
- [Compare Sisyphus set dependencies with `newset.c`](Docs/compare-sisyphus-set-versions.md)
+611
View File
@@ -0,0 +1,611 @@
#!/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"
+10
View File
@@ -0,0 +1,10 @@
BEGIN { FS = "\t" }
NF == 11 && $2 == "START" {
version[$3 "\034" $4] = $11
}
NF == 11 && $2 == "SUMMARY" && $11 ~ /(^|; )complete=1$/ && \
version[$3 "\034" $4] != "" {
print $3 "\t" $4 "\t" version[$3 "\034" $4]
}
+48
View File
@@ -0,0 +1,48 @@
#ifndef ARSV_NEWSET_COMPAT_H
#define ARSV_NEWSET_COMPAT_H
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static inline void *arsv_xmalloc(size_t size)
{
void *value = malloc(size);
if (!value) {
fputs("newset: out of memory\n", stderr);
abort();
}
return value;
}
static inline void *arsv_xrealloc(void *pointer, size_t size)
{
void *value = realloc(pointer, size);
if (!value) {
fputs("newset: out of memory\n", stderr);
abort();
}
return value;
}
static inline char *arsv_xstrdup(const char *source)
{
size_t size = strlen(source) + 1;
char *copy = (char *)arsv_xmalloc(size);
memcpy(copy, source, size);
return copy;
}
static inline void *arsv_free(void *pointer)
{
free(pointer);
return NULL;
}
#define xmalloc arsv_xmalloc
#define xrealloc arsv_xrealloc
#define xstrdup arsv_xstrdup
#define _free arsv_free
#endif
+63
View File
@@ -0,0 +1,63 @@
/* mkset-compatible wrapper around reimplement/newset.c.
* The stdin/argv contract matches rpm-build/tools/mkset.c. */
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
struct set;
struct set *set_new(void);
void set_add(struct set *set, const char *symbol);
const char *set_fini(struct set *set, int bpp);
struct set *set_free(struct set *set);
int main(int argc, char **argv)
{
if (argc != 2) {
fprintf(stderr, "usage: %s BPP\n", argv[0]);
return 2;
}
char *end = NULL;
errno = 0;
long parsed_bpp = strtol(argv[1], &end, 10);
if (errno || !end || *end || parsed_bpp < 10 || parsed_bpp > 32) {
fprintf(stderr, "invalid BPP: %s (expected 10..32)\n", argv[1]);
return 2;
}
int bpp = (int)parsed_bpp;
struct set *set = set_new();
char *line = NULL;
size_t allocated = 0;
ssize_t length;
int added = 0;
while ((length = getline(&line, &allocated, stdin)) >= 0) {
if (length > 0 && line[length - 1] == '\n')
line[--length] = '\0';
if (length == 0)
continue;
set_add(set, line);
++added;
}
if (!added) {
fputs("mkset: no symbols on standard input\n", stderr);
free(line);
set_free(set);
return 2;
}
const char *encoded = set_fini(set, bpp);
if (!encoded) {
fputs("mkset: unable to encode set\n", stderr);
free(line);
set_free(set);
return 1;
}
printf("set:%s\n", encoded);
free((void *)encoded);
free(line);
set_free(set);
return 0;
}
+39
View File
@@ -0,0 +1,39 @@
BEGIN {
RS = ""
FS = "\n"
OFS = "\034"
}
{
package = architecture = version = filename = md5 = ""
has_provides = has_requires = 0
dependency_field = ""
for (i = 1; i <= NF; ++i) {
if ($i ~ /^Package: /)
package = substr($i, 10)
else if ($i ~ /^Architecture: /)
architecture = substr($i, 15)
else if ($i ~ /^Version: /)
version = substr($i, 10)
else if ($i ~ /^Filename: /)
filename = substr($i, 11)
else if ($i ~ /^MD5Sum: /)
md5 = substr($i, 9)
if ($i ~ /^Provides: /)
dependency_field = "provides"
else if ($i ~ /^(Pre-Depends|Depends): /)
dependency_field = "requires"
else if ($i !~ /^[[:space:]]/)
dependency_field = ""
if (dependency_field == "provides" && index($i, "set:"))
has_provides = 1
if (dependency_field == "requires" && index($i, "set:"))
has_requires = 1
}
if (package != "" && architecture != "" && version != "" && filename != "")
print package, architecture, version, filename, md5, has_provides, has_requires
}
+126
View File
@@ -0,0 +1,126 @@
#!/bin/bash
rpm_safe_symlink_target()
{
local root=$1 relative_path=$2 link_target=$3
local canonical_root link_directory resolved_target
canonical_root=$(realpath -m -- "$root") || return 1
link_directory=$(realpath -m -- "$canonical_root/$(dirname -- "$relative_path")") || return 1
if [[ $link_target == /* ]]; then
resolved_target=$(realpath -m -- "$canonical_root/${link_target#/}") || return 1
else
resolved_target=$(realpath -m -- "$link_directory/$link_target") || return 1
fi
[[ $resolved_target == "$canonical_root" || $resolved_target == "$canonical_root"/* ]] || return 1
if [[ $link_target == /* ]]; then
realpath -m --relative-to="$link_directory" -- "$resolved_target"
else
printf '%s\n' "$link_target"
fi
}
rpm_cpio_literal_pattern()
{
local input=$1 output= character index
[[ $input != *$'\n'* && $input != *$'\r'* ]] || return 1
for ((index = 0; index < ${#input}; ++index)); do
character=${input:index:1}
case $character in
'[') output+='[[]' ;;
']') output+='[]]' ;;
'*') output+='[*]' ;;
'?') output+='[?]' ;;
'\\') output+='[\\]' ;;
*) output+=$character ;;
esac
done
printf '%s\n' "$output"
}
rpm_split_file_record()
{
local record=$1 separator=$'\034'
RPM_FILENAME=
RPM_LINK_TARGET=
[[ $record == *"$separator"* ]] || return 1
RPM_FILENAME=${record%%"$separator"*}
RPM_LINK_TARGET=${record#*"$separator"}
[[ $RPM_LINK_TARGET != *"$separator"* ]] || return 1
[[ $RPM_FILENAME != *$'\t'* && $RPM_FILENAME != *$'\n'* && $RPM_FILENAME != *$'\r'* ]] || return 1
[[ $RPM_LINK_TARGET != *$'\t'* && $RPM_LINK_TARGET != *$'\n'* && $RPM_LINK_TARGET != *$'\r'* ]] || return 1
}
rpm_install_symlink_manifest()
{
local root=$1 manifest=$2 log=$3 separator=$'\034'
local canonical_root sorted row relative link_target safe_target destination parent existing
canonical_root=$(realpath -m -- "$root") || return 1
sorted=$(mktemp "${manifest}.sorted.XXXXXX") || return 1
if ! awk -F '\t' -v separator="$separator" '
NF >= 2 {
path=$1
depth=gsub(/\//, "/", path)
printf "%08d%s%s\n", depth, separator, $0
}
' "$manifest" | LC_ALL=C sort -t "$separator" -k1,1n -k2,2 >"$sorted"; then
rm -f -- "$sorted"
return 1
fi
while IFS=$separator read -r _depth row; do
IFS=$'\t' read -r relative link_target <<<"$row"
[[ -n $relative && -n $link_target ]] || continue
destination=$canonical_root/$relative
parent=$(realpath -m -- "$(dirname -- "$destination")") || {
rm -f -- "$sorted"
return 1
}
[[ $parent == "$canonical_root" || $parent == "$canonical_root"/* ]] || {
printf 'escaping RPM symlink parent rejected: %q\n' "/$relative" >>"$log"
rm -f -- "$sorted"
return 1
}
if ! safe_target=$(rpm_safe_symlink_target "$canonical_root" "$relative" "$link_target"); then
printf 'escaping RPM symlink rejected: %q -> %q\n' "/$relative" "$link_target" >>"$log"
rm -f -- "$sorted"
return 1
fi
mkdir -p -- "$parent" || {
rm -f -- "$sorted"
return 1
}
if [[ -L $destination ]]; then
existing=$(readlink -- "$destination") || {
rm -f -- "$sorted"
return 1
}
if [[ $existing == "$safe_target" ]]; then
continue
fi
printf 'conflicting RPM symlinks: %q -> %q and %q\n' \
"/$relative" "$existing" "$safe_target" >>"$log"
rm -f -- "$sorted"
return 1
elif [[ -e $destination ]]; then
printf 'RPM symlink conflicts with existing path: %q\n' "/$relative" >>"$log"
rm -f -- "$sorted"
return 1
fi
if ! ln -s -- "$safe_target" "$destination"; then
printf 'unable to create RPM symlink: %q -> %q\n' "/$relative" "$safe_target" >>"$log"
rm -f -- "$sorted"
return 1
fi
done <"$sorted"
rm -f -- "$sorted"
}
+319
View File
@@ -0,0 +1,319 @@
from __future__ import annotations
import os
import subprocess
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPT = REPO_ROOT / "scripts" / "compare_sisyphus_set_versions.sh"
PACKAGE_PARSER = REPO_ROOT / "scripts" / "parse_sisyphus_packages.awk"
RESUME_PARSER = REPO_ROOT / "scripts" / "completed_set_versions.awk"
PAYLOAD_HELPERS = REPO_ROOT / "scripts" / "rpm_payload_safety.sh"
def run_script(*arguments: str, input_text: str | None = None) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["bash", str(SCRIPT), *arguments],
cwd=REPO_ROOT,
text=True,
input=input_text,
capture_output=True,
)
def test_requires_explicit_scope_before_touching_repository() -> None:
completed = run_script()
assert completed.returncode == 2
assert "use --all, --limit, or --package" in completed.stderr
def test_rejects_source_list_and_package_name_injection_before_network() -> None:
bad_mirror = run_script("--package", "strace", "--mirror", "https://example.test/\nrpm evil")
assert bad_mirror.returncode == 2
assert "mirror must not contain whitespace" in bad_mirror.stderr
bad_package = run_script("--package", "--option")
assert bad_package.returncode == 2
assert "invalid package name" in bad_package.stderr
def test_build_mkset_wraps_newset_and_matches_known_alt_payload(tmp_path: Path) -> None:
output = tmp_path / "new-mkset"
completed = run_script("--build-mkset", str(output))
assert completed.returncode == 0, completed.stderr
assert output.is_file()
assert os.access(output, os.X_OK)
encoded = subprocess.run(
[str(output), "16"],
input="sym0\nsym1\nsym2\nsym3\nsym0\n",
text=True,
capture_output=True,
check=True,
)
assert encoded.stdout == "set:jg2ZwFjdTD2B0\n"
bad_bpp = subprocess.run(
[str(output), "16junk"],
input="sym0\n",
text=True,
capture_output=True,
)
assert bad_bpp.returncode == 2
assert "invalid BPP" in bad_bpp.stderr
empty = subprocess.run(
[str(output), "16"],
input="",
text=True,
capture_output=True,
)
assert empty.returncode == 2
assert "no symbols" in empty.stderr
def test_package_parser_preserves_empty_md5_field() -> None:
metadata = """\
Package: libexample
Architecture: x86_64
Version: 1.2-alt1
Filename: libexample-1.2-alt1.x86_64.rpm
Provides: libexample.so.1 = set:abc
Depends: libc.so.6
libc.so.6(GLIBC_2.34)(64bit) >= set:def
"""
completed = subprocess.run(
["awk", "-f", str(PACKAGE_PARSER)],
input=metadata,
text=True,
capture_output=True,
check=True,
)
assert completed.stdout.rstrip("\n").split("\x1c") == [
"libexample",
"x86_64",
"1.2-alt1",
"libexample-1.2-alt1.x86_64.rpm",
"",
"1",
"1",
]
def test_resume_parser_ignores_partial_summary_rows() -> None:
report = "\n".join(
[
"timestamp\trecord\tpackage\tarchitecture\tstatus\tside\tcapability\toperator\texpected\tgenerated\tdetail",
"t\tSTART\tbroken\tx86_64\tprocessing\t-\t-\t-\t-\t-\t1-alt1",
"t\tSUMMARY\tbroken\tx86_64\tmatch",
"t\tSTART\tcomplete\tnoarch\tprocessing\t-\t-\t-\t-\t-\t2-alt1",
"t\tSUMMARY\tcomplete\tnoarch\tmatch\t-\t-\t-\t-\t-\tdone; complete=1",
"",
]
)
completed = subprocess.run(
["awk", "-f", str(RESUME_PARSER)],
input=report,
text=True,
capture_output=True,
check=True,
)
assert completed.stdout == "complete\tnoarch\t2-alt1\n"
def test_absolute_rpm_symlink_is_rewritten_inside_temporary_root(tmp_path: Path) -> None:
root = tmp_path / "root"
(root / "usr" / "sbin").mkdir(parents=True)
completed = subprocess.run(
[
"bash",
"-c",
'source "$1"; rpm_safe_symlink_target "$2" "$3" "$4"',
"bash",
str(PAYLOAD_HELPERS),
str(root),
"usr/sbin/update-alternatives",
"/bin/true",
],
text=True,
capture_output=True,
)
assert completed.returncode == 0, completed.stderr
assert completed.stdout == "../../bin/true\n"
def test_escaping_relative_rpm_symlink_is_rejected(tmp_path: Path) -> None:
root = tmp_path / "root"
root.mkdir()
completed = subprocess.run(
[
"bash",
"-c",
'source "$1"; rpm_safe_symlink_target "$2" "$3" "$4"',
"bash",
str(PAYLOAD_HELPERS),
str(root),
"usr/link",
"../../../outside",
],
text=True,
capture_output=True,
)
assert completed.returncode != 0
def test_cpio_member_pattern_quotes_glob_characters() -> None:
completed = subprocess.run(
[
"bash",
"-c",
'source "$1"; rpm_cpio_literal_pattern "$2"',
"bash",
str(PAYLOAD_HELPERS),
"./usr/share/doc/a[1]*?.txt",
],
text=True,
capture_output=True,
)
assert completed.returncode == 0, completed.stderr
assert completed.stdout == "./usr/share/doc/a[[]1[]][*][?].txt\n"
def test_directory_symlinks_are_installed_before_children(tmp_path: Path) -> None:
root = tmp_path / "root"
root.mkdir()
manifest = tmp_path / "symlinks.tsv"
manifest.write_text("bin/sh\tbash\nbin\tusr/bin\n")
completed = subprocess.run(
[
"bash",
"-c",
'source "$1"; rpm_install_symlink_manifest "$2" "$3" "$4"',
"bash",
str(PAYLOAD_HELPERS),
str(root),
str(manifest),
str(tmp_path / "extract.log"),
],
text=True,
capture_output=True,
)
assert completed.returncode == 0, completed.stderr
assert (root / "bin").is_symlink()
assert os.readlink(root / "bin") == "usr/bin"
assert (root / "usr" / "bin" / "sh").is_symlink()
assert os.readlink(root / "usr" / "bin" / "sh") == "bash"
def test_child_target_is_revalidated_after_parent_alias(tmp_path: Path) -> None:
root = tmp_path / "root"
root.mkdir()
manifest = tmp_path / "symlinks.tsv"
manifest.write_text("deep/a\t..\ndeep/a/link\t../outside\n")
completed = subprocess.run(
[
"bash",
"-c",
'source "$1"; rpm_install_symlink_manifest "$2" "$3" "$4"',
"bash",
str(PAYLOAD_HELPERS),
str(root),
str(manifest),
str(tmp_path / "extract.log"),
],
text=True,
capture_output=True,
)
assert completed.returncode != 0
assert not (tmp_path / "outside").exists()
def test_absolute_target_is_rewritten_after_parent_alias(tmp_path: Path) -> None:
root = tmp_path / "root"
root.mkdir()
manifest = tmp_path / "symlinks.tsv"
manifest.write_text("bin\tusr/bin\nbin/tool\t/opt/tool\n")
completed = subprocess.run(
[
"bash",
"-c",
'source "$1"; rpm_install_symlink_manifest "$2" "$3" "$4"',
"bash",
str(PAYLOAD_HELPERS),
str(root),
str(manifest),
str(tmp_path / "extract.log"),
],
text=True,
capture_output=True,
)
assert completed.returncode == 0, completed.stderr
assert os.readlink(root / "usr" / "bin" / "tool") == "../../opt/tool"
assert (root / "usr" / "bin" / "tool").resolve(strict=False) == root / "opt" / "tool"
def test_symlinked_temporary_root_is_canonicalized(tmp_path: Path) -> None:
real_root = tmp_path / "real-root"
real_root.mkdir()
root_alias = tmp_path / "root-alias"
root_alias.symlink_to(real_root, target_is_directory=True)
manifest = tmp_path / "symlinks.tsv"
manifest.write_text("bin\tusr/bin\n")
completed = subprocess.run(
[
"bash",
"-c",
'source "$1"; rpm_install_symlink_manifest "$2" "$3" "$4"',
"bash",
str(PAYLOAD_HELPERS),
str(root_alias),
str(manifest),
str(tmp_path / "extract.log"),
],
text=True,
capture_output=True,
)
assert completed.returncode == 0, completed.stderr
assert (real_root / "bin").is_symlink()
def test_rpm_file_record_with_tab_in_filename_is_rejected() -> None:
record = "/name\tpart\x1c"
completed = subprocess.run(
[
"bash",
"-c",
'source "$1"; rpm_split_file_record "$2"',
"bash",
str(PAYLOAD_HELPERS),
record,
],
text=True,
capture_output=True,
)
assert completed.returncode != 0
def test_help_documents_streaming_report_and_test_scope() -> None:
completed = run_script("--help")
assert completed.returncode == 0
assert "--report FILE" in completed.stdout
assert "--package NAME" in completed.stdout
assert "--limit N" in completed.stdout
assert "--all" in completed.stdout
assert "appended after every package" in completed.stdout
+17
View File
@@ -0,0 +1,17 @@
2026-07-19T03:40:59Z START ORBit2 x86_64 processing - - - - - 2.14.20-alt0.4:sisyphus+361606.200.1.1@1730563927
2026-07-19T03:41:09Z DEPENDENCY ORBit2 x86_64 extra_generated requires libORBit-2.so.0()(64bit) >= set:niDtqCAHBjYjaJDKEoB0hQrUcD1RWQ8Y9dT4Ll8IwTKKrUFvFOcPooPnZzZqghuX9GWYZi3 -
2026-07-19T03:41:09Z DEPENDENCY ORBit2 x86_64 match requires libglib-2.0.so.0()(64bit) >= set:ojfwe09N7dwsn61JFrFyGiav6VPWMh6BXOl0ykouiiezwiOhIbG0 set:ojfwe09N7dwsn61JFrFyGiav6VPWMh6BXOl0ykouiiezwiOhIbG0 -
2026-07-19T03:41:09Z SUMMARY ORBit2 x86_64 mismatch - - - - - requires expected=1 generated=2 differences=0 extras=1; complete=1
2026-07-19T03:48:54Z DEPENDENCY SILLY x86_64 mismatch requires libjpeg.so.62()(64bit) >= set:jfvZkgcUQqm1WcNFYdT6Zh set:kgvLPFlp5EP4DOJVCPR1WC6 -
2026-07-19T03:48:54Z DEPENDENCY SILLY x86_64 match requires libpng16.so.16()(64bit) >= set:lhkC1fmC6EFQ5Klh4dezRCyvQFOXIZ3uwhXdUhBK49 set:lhkC1fmC6EFQ5Klh4dezRCyvQFOXIZ3uwhXdUhBK49 -
2026-07-19T03:58:11Z START abuse x86_64 processing - - - - - 0.8-alt2@1308659965
2026-07-19T03:58:33Z DEPENDENCY abuse x86_64 match requires libSDL-1.2.so.0()(64bit) >= set:lgkZiCZcFvTDDX6Y1PaRgCdFuVAwizcZfaTezfxZFUxcDOoVbbKRxY6ahoMjPQkxX5fdZtjAKGEY4 set:lgkZiCZcFvTDDX6Y1PaRgCdFuVAwizcZfaTezfxZFUxcDOoVbbKRxY6ahoMjPQkxX5fdZtjAKGEY4 -
2026-07-19T03:58:33Z DEPENDENCY abuse x86_64 mismatch requires libSDL_mixer-1.2.so.0()(64bit) >= set:lgIdCOqw7JFqcojADYwCZ7pQY3CJ1LnAqVzI3Z0QXq0pLy1 set:kfMSoaHVyGBhV7CJ1YuVyx95uAdQPsq0b5zW6rUlsY3 -
2026-07-19T03:58:33Z SUMMARY abuse x86_64 mismatch - - - - - requires expected=2 generated=2 differences=1 extras=0; complete=1
2026-07-19T04:08:20Z DEPENDENCY airstrike x86_64 mismatch requires libSDL_mixer-1.2.so.0()(64bit) >= set:lirzppdVhrMiF4ChTZdaR8 set:khS6jpdPZrxpQlg2V9Bq4 -