From af4726dcef9fc94300f61b1abeefe74c0bb5d914 Mon Sep 17 00:00:00 2001 From: noctuum <25441068+noctuum@users.noreply.github.com> Date: Fri, 16 Oct 2026 05:20:19 +0500 Subject: [PATCH] fix(scripts): let the guards pass valid updates --- docs/WORKFLOW.md | 4 +- scripts/lib.sh | 35 ++++++++++++---- tests/publish.bats | 101 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 9 deletions(-) diff --git a/docs/WORKFLOW.md b/docs/WORKFLOW.md index 1e86f5e..742abc7 100644 --- a/docs/WORKFLOW.md +++ b/docs/WORKFLOW.md @@ -635,8 +635,8 @@ A PKGBUILD is a bash script, and every step that reads it executes it: the audit 1. The prepared directory must not be a symlink, `PKGBUILD`, `.SRCINFO` and `base` must be regular files, and `base` a commit id. 2. When the prepared `pkgver` is not ``, upstream moved after the update was prepared: exit code 3. 3. Clone over SSH. When `HEAD` is not `base`, the AUR repo moved after the update was prepared: exit code 3. `PKGBUILD` and `.SRCINFO` in the clone must be regular files, not symlinks. -4. The prepared `PKGBUILD` may differ from the AUR copy only in `pkgver=`, which must be ``, in `pkgrel=`, which must be `1`, and in the values of the checksum arrays. The arrays keep their names and lengths, hold only quoted hex checksums or `SKIP`, and have a `SKIP` exactly where the AUR copy has one. -5. Each line of the prepared `.SRCINFO` must be the line of the AUR copy with the old version replaced by ``. The same holds for three forms a PKGBUILD often derives from `pkgver`: dots turned into underscores (`1_86_0`), the part before the first `+` (`0.13.0` of `0.13.0+dev+1853+g6edb38ca98`), and the version without its last component (`6.19` of `6.19.0`, as in `frameworks/${pkgver%.*}/` of KDE Frameworks). The last form counts only while it keeps a dot. Each part of a line is replaced at most once, so `5.1` inside an already replaced `5.10.0` stays. A line built from any other form of the version, a lone major version for example, stops the publish, and the package needs a manual update. `pkgrel` and the checksums follow the rules of step 4. +4. The prepared `PKGBUILD` may differ from the AUR copy only in `pkgver=`, which must be ``, in `pkgrel=`, which must be `1`, and in the values of the checksum arrays. The arrays keep their names and lengths, hold only quoted hex checksums or `SKIP`, and have a `SKIP` exactly where the AUR copy has one. Comments and line continuations may stand between the checksums, read as bash reads them: a `#` at the start of a line, right after the opening parenthesis, or after a space or a tab comments out the rest of the line, and a backslash that ends a line without a comment, as a word of its own, continues the array on the next line. A backslash on the line that closes the array stops the publish, because bash joins the next line to the assignment. +5. Each `source` line of the prepared `.SRCINFO`, with or without an arch suffix, must be the line of the AUR copy with the old version replaced by ``. Any other line must either equal the line of the AUR copy or be that line with the version replaced in the same way, because a license, a dependency or a description can hold the old version without taking it from `pkgver`, as `GPL-2.0` does in a package at version `2.0`. The replacement also covers three forms a PKGBUILD often derives from `pkgver`: dots turned into underscores (`1_86_0`), the part before the first `+` (`0.13.0` of `0.13.0+dev+1853+g6edb38ca98`), and the version without its last component (`6.19` of `6.19.0`, as in `frameworks/${pkgver%.*}/` of KDE Frameworks). The last form counts only while it keeps a dot. Each part of a line is replaced at most once, so `5.1` inside an already replaced `5.10.0` stays. A line built from any other form of the version, a lone major version for example, stops the publish, and so does a line that has the version replaced in one place and kept in another. The package then needs a manual update. `pkgrel` and the checksums follow the rules of step 4. 6. Any other difference, and a NUL byte in either file, stops the publish with exit code 1. Otherwise steps 10 to 13 of [4.2](#42-step-by-step) follow. When the prepared files equal the AUR copy, only `current` is written to the YAML and the exit code is 2. `check-all.sh --prepare ` runs no strategy and needs no `GITHUB_TOKEN`. It takes the versions from the statuses that a check run, `check-all.sh` without a mode, wrote to `.status/` before it, and reads them before the first PKGBUILD runs. It prepares every package with `new_version` or `available_downgrade` into `/prepared/` and copies the statuses to `/status/`. A failed preparation becomes `update_failed`, and a package without a status from the check run becomes `check_failed`. The other statuses stay as the check run wrote them. diff --git a/scripts/lib.sh b/scripts/lib.sh index d72aa1e..cde8b3a 100644 --- a/scripts/lib.sh +++ b/scripts/lib.sh @@ -360,7 +360,8 @@ _has_nul() { # PKGVER|PKGRELval the value of a pkgver= or pkgrel= line # SUMarrayindexHASH|SKIP one line per checksum # SUMVALarrayvalue the same checksum with its value -# BADtext anything in a checksum array but a quoted checksum +# BADtext anything in a checksum array but a quoted +# checksum, a comment or a line continuation # Args: $1=pkgbuild path _pkgbuild_parts() { awk ' @@ -377,14 +378,22 @@ _pkgbuild_parts() { print "SUMVAL\t" name "\t" substr(t, 2, length(t) - 2) } } - function body(text, end) { + # As bash does: a # that starts a word comments out the rest of the + # line, and a backslash at the end of a line outside a comment joins + # the next line, which inside the parentheses is only a separator + function body(text, raw, cont, end, rest) { + raw = text + sub(/(^|[ \t])#.*/, "", text) + cont = text == raw && text ~ /(^|[ \t])\\$/ + if (cont) text = substr(text, 1, length(text) - 1) end = index(text, ")") if (!end) { tokens(text) return 1 } tokens(substr(text, 1, end - 1)) - if (substr(text, end + 1) !~ /^[ \t]*$/) print "BAD\t" NR ": " substr(text, end + 1) + rest = substr(text, end + 1) (cont ? "\\" : "") + if (rest !~ /^[ \t]*$/) print "BAD\t" NR ": " rest return 0 } in_sums { in_sums = body($0); next } @@ -418,7 +427,7 @@ check_prepared_pkgbuild() { fi if grep -q $'^BAD\t' <<<"$new_parts"; then - log_err "Prepared PKGBUILD has more than quoted checksums in a checksum array:" + log_err "Prepared PKGBUILD has more than quoted checksums, comments and line continuations in a checksum array:" grep $'^BAD\t' <<<"$new_parts" | cut -f2- >&2 return 1 fi @@ -495,8 +504,12 @@ _bump_version() { } # A .SRCINFO is generated, so line N of the prepared file must be line N of -# the AUR file with the old version replaced by the new one, see -# _bump_version. pkgrel and the checksum values are the exceptions. +# the AUR file, with these changes: pkgver is the new version, pkgrel is 1, +# a checksum keeps its key and stays a SKIP or a hash, a source has the old +# version replaced by the new one (see _bump_version), and any other line +# either stays or has the version replaced in the same way. A license, a +# dependency or a pkgdesc can hold the old version without deriving it +# from pkgver. # Args: $1=AUR .SRCINFO, $2=prepared .SRCINFO, $3=expected version # Returns: 0 when that holds, 1 otherwise (reason on stderr) check_prepared_srcinfo() { @@ -532,6 +545,8 @@ check_prepared_srcinfo() { kind="$key" elif [[ "$key" =~ ^(ck|md5|sha1|sha224|sha256|sha384|sha512|b2)sums(_[A-Za-z0-9_]+)?$ ]]; then kind=sums + elif [[ "$key" =~ ^source(_[A-Za-z0-9_]+)?$ ]]; then + kind=source else kind=other fi @@ -560,8 +575,14 @@ check_prepared_srcinfo() { return 1 fi ;; - *) + source) if [[ "$new" != "$(_bump_version "$old" "$old_ver" "$version")" ]]; then + log_err "Prepared .SRCINFO line $((i + 1)) is not the AUR source with the new version: $new" + return 1 + fi + ;; + *) + if [[ "$new" != "$old" && "$new" != "$(_bump_version "$old" "$old_ver" "$version")" ]]; then log_err "Prepared .SRCINFO line $((i + 1)) changes more than the version: $new" return 1 fi diff --git a/tests/publish.bats b/tests/publish.bats index ae8077d..8aeffb9 100644 --- a/tests/publish.bats +++ b/tests/publish.bats @@ -1174,6 +1174,58 @@ EOF [ "$status" -eq 0 ] } +@test "check_prepared_pkgbuild: accepts a checksum array continued with a backslash" { + local sums="sha256sums=('SKIP' \\ + 'SKIP')" + write_pkgbuild "$OLD" 1.0.0 2 "$sums" + write_pkgbuild "$NEW" 1.1.0 1 "$sums" + run check_prepared_pkgbuild "$OLD" "$NEW" 1.1.0 + [ "$status" -eq 0 ] +} + +@test "check_prepared_pkgbuild: accepts a comment after a checksum array" { + local sums="sha256sums=('SKIP' 'SKIP') # VCS sources" + write_pkgbuild "$OLD" 1.0.0 2 "$sums" + write_pkgbuild "$NEW" 1.1.0 1 "$sums" + run check_prepared_pkgbuild "$OLD" "$NEW" 1.1.0 + [ "$status" -eq 0 ] +} + +@test "check_prepared_pkgbuild: accepts comments inside a checksum array" { + local sums="sha256sums=( # git + 'SKIP' # patch (local) + 'SKIP')" + write_pkgbuild "$OLD" 1.0.0 2 "$sums" + write_pkgbuild "$NEW" 1.1.0 1 "$sums" + run check_prepared_pkgbuild "$OLD" "$NEW" 1.1.0 + [ "$status" -eq 0 ] +} + +# bash joins the next line to the assignment, and runs it as a command +@test "check_prepared_pkgbuild: rejects a backslash after a checksum array" { + write_pkgbuild "$OLD" 1.0.0 2 "sha256sums=('SKIP' 'SKIP')" + write_pkgbuild "$NEW" 1.1.0 1 "sha256sums=('SKIP' 'SKIP') \\" + run check_prepared_pkgbuild "$OLD" "$NEW" 1.1.0 + [ "$status" -eq 1 ] +} + +# bash reads an escaped space there, so the # starts a word, not a comment +@test "check_prepared_pkgbuild: rejects a backslash before a hash sign in a checksum array" { + write_pkgbuild "$OLD" 1.0.0 2 "sha256sums=('SKIP' 'SKIP')" + write_pkgbuild "$NEW" 1.1.0 1 "sha256sums=('SKIP' \\ # c + 'SKIP')" + run check_prepared_pkgbuild "$OLD" "$NEW" 1.1.0 + [ "$status" -eq 1 ] +} + +# bash starts no comment right after the closing parenthesis +@test "check_prepared_pkgbuild: rejects a hash sign right after a checksum array" { + write_pkgbuild "$OLD" 1.0.0 2 "sha256sums=('SKIP' 'SKIP')" + write_pkgbuild "$NEW" 1.1.0 1 "sha256sums=('SKIP' 'SKIP')# c" + run check_prepared_pkgbuild "$OLD" "$NEW" 1.1.0 + [ "$status" -eq 1 ] +} + @test "check_prepared_pkgbuild: rejects a changed number of checksums" { write_pkgbuild "$OLD" 1.0.0 2 "sha256sums=('$H1' '$H2')" write_pkgbuild "$NEW" 1.1.0 1 "sha256sums=('$H3')" @@ -1278,6 +1330,55 @@ write_srcinfo_line() { [ "$status" -eq 1 ] } +# Helper: .SRCINFO as makepkg --printsrcinfo prints it for a PKGBUILD with +# pkgdesc, license and depends holding 2.0 apart from pkgver +# Usage: write_srcinfo_lookalike +write_srcinfo_lookalike() { + printf '%s\n' \ + "pkgbase = demo" \ + $'\tpkgdesc = A GPL-2.0 licensed tool' \ + $'\tpkgver = '"$2" \ + $'\tpkgrel = 1' \ + $'\turl = https://example.com' \ + $'\tarch = x86_64' \ + $'\tlicense = GPL-2.0-or-later' \ + $'\tdepends = glibc>=2.0.1' \ + $'\tprovides = tool='"$2" \ + $'\tsource = https://example.com/releases/'"$2"'/demo-'"$2"'.tar.gz' \ + $'\tsha256sums = SKIP' \ + "" \ + "pkgname = demo" >"$1" +} + +@test "check_prepared_srcinfo: keeps text that only looks like the old version" { + write_srcinfo_lookalike "$TEST_TMPDIR/old.SRCINFO" 2.0 + write_srcinfo_lookalike "$TEST_TMPDIR/new.SRCINFO" 2.1 + run check_prepared_srcinfo "$TEST_TMPDIR/old.SRCINFO" "$TEST_TMPDIR/new.SRCINFO" 2.1 + [ "$status" -eq 0 ] +} + +@test "check_prepared_srcinfo: rejects a source that keeps the old version" { + write_srcinfo_line "$TEST_TMPDIR/old.SRCINFO" 2.0 "source = https://example.com/demo-2.0.tar.gz" + write_srcinfo_line "$TEST_TMPDIR/new.SRCINFO" 2.1 "source = https://example.com/demo-2.0.tar.gz" + run check_prepared_srcinfo "$TEST_TMPDIR/old.SRCINFO" "$TEST_TMPDIR/new.SRCINFO" 2.1 + [ "$status" -eq 1 ] +} + +@test "check_prepared_srcinfo: rejects an arch source that keeps the old version" { + write_srcinfo_line "$TEST_TMPDIR/old.SRCINFO" 2.0 "source_x86_64 = https://example.com/demo-2.0-x86_64.tar.gz" + write_srcinfo_line "$TEST_TMPDIR/new.SRCINFO" 2.1 "source_x86_64 = https://example.com/demo-2.0-x86_64.tar.gz" + run check_prepared_srcinfo "$TEST_TMPDIR/old.SRCINFO" "$TEST_TMPDIR/new.SRCINFO" 2.1 + [ "$status" -eq 1 ] +} + +@test "check_prepared_srcinfo: rejects a line changed beyond the version" { + write_srcinfo_line "$TEST_TMPDIR/old.SRCINFO" 2.0 "depends = glibc>=2.0.1" + write_srcinfo_line "$TEST_TMPDIR/new.SRCINFO" 2.1 "depends = glibc>=2.9" + run check_prepared_srcinfo "$TEST_TMPDIR/old.SRCINFO" "$TEST_TMPDIR/new.SRCINFO" 2.1 + [ "$status" -eq 1 ] + [[ "$output" == *"changes more than the version"* ]] +} + @test "check_prepared_srcinfo: rejects an added dependency" { write_srcinfo "$TEST_TMPDIR/old.SRCINFO" 1.0.0 2 "$H1" "$H2" write_srcinfo "$TEST_TMPDIR/new.SRCINFO" 1.1.0 1 "$H3" "$H4"