Make the upstream rewrite verify its own result

A second review pass found the PKGBUILD rewriting could still go wrong in ways
the pattern matching did not anticipate: an array element carrying a ")" in a
comment left the tail of the old array behind, and jq's "$" also matches before
a trailing newline, so a pkgver of "1.0\n" passed validation and then broke sed
after the checksum arrays had already been written.

Rather than chase each shape, prove the result. Every edit now lands on a
scratch copy that is parsed with bash -n and read back to confirm it holds the
version and checksums we meant to write, and only then replaces the PKGBUILD in
a single rename. Corruption that slips past the matching fails loudly with the
original untouched instead of landing in a pull request.

The validation anchors are \A and \z accordingly, empty checksum lists are
rejected rather than written as '', and the hook picks the newest stanza with
vercmp so it agrees with the comparator the updater uses.

Also stop the launcher probing /.config when HOME and XDG_CONFIG_HOME are both
unset, and require a regular file, so a directory at that path is skipped
instead of crashing the app on startup.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-08-15 08:34:42 -07:00
co-authored by Claude Opus 5
parent 1a61278911
commit f92de9c440
4 changed files with 127 additions and 38 deletions
+108 -32
View File
@@ -104,17 +104,23 @@ set_pkgbuild_array() {
assert_single_assignment "$pkgbuild" "^${name}=(" "$name" || return 1
local block="$TEMP_DIR/array-block"
if [[ ${#values[@]} -eq 1 ]]; then
printf "%s=('%s')\n" "$name" "${values[0]}" > "$block"
else
printf '%s=(\n' "$name" > "$block"
printf " '%s'\n" "${values[@]}" >> "$block"
printf ')\n' >> "$block"
if [[ ${#values[@]} -eq 0 ]]; then
print_error "No values to write for ${name}"
return 1
fi
# Rewrite beside the PKGBUILD so the move is an atomic same-filesystem rename.
local rewritten="$pkgbuild.sync-upstream"
local block="$TEMP_DIR/array-block"
if [[ ${#values[@]} -eq 1 ]]; then
printf "%s=('%s')\n" "$name" "${values[0]}" > "$block" || return 1
else
{
printf '%s=(\n' "$name"
printf " '%s'\n" "${values[@]}"
printf ')\n'
} > "$block" || return 1
fi
local rewritten="$TEMP_DIR/pkgbuild-rewritten"
if ! awk -v prefix="${name}=(" -v block="$block" '
!replaced && index($0, prefix) == 1 {
while ((getline line < block) > 0) print line
@@ -134,7 +140,6 @@ set_pkgbuild_array() {
return 1
fi
chmod --reference="$pkgbuild" "$rewritten"
mv "$rewritten" "$pkgbuild"
}
@@ -154,18 +159,71 @@ validate_release() {
# pkgver is written into the PKGBUILD, which makepkg sources as shell, so it
# is held to pacman's own character set rather than merely being non-empty.
# The anchors are \A and \z, not ^ and $: jq's $ also matches before a
# trailing newline, which would let "1.0\n" through and break the rewrite.
jq -e '
(.pkgver | type == "string" and test("^[A-Za-z0-9._+]+$"))
(.pkgver | type == "string" and test("\\A[A-Za-z0-9._+]+\\z"))
and (.sha256sums | type == "object" and length > 0)
and (.sha256sums | to_entries | all(
.key | test("^[a-z0-9_]+$")
.key | test("\\A[a-z0-9_]+\\z")
))
and (.sha256sums | to_entries | all(
.value | type == "array" and length > 0 and all(test("^[0-9a-f]{64}$"))
.value | type == "array" and length > 0 and all(test("\\A[0-9a-f]{64}\\z"))
))
' <<<"$release" >/dev/null
}
# Confirm the rewritten PKGBUILD parses and actually holds what we meant to put
# in it. Editing shell with awk and sed can go wrong in ways no amount of
# pattern-matching anticipates -- an array element carrying a ")" in a comment,
# say -- so the result is checked rather than trusted.
verify_pkgbuild() {
local pkgbuild="$1"
local release="$2"
local pkgver="$3"
shift 3
local arrays=("$@")
if ! bash -n "$pkgbuild" 2>/dev/null; then
print_error "Rewritten PKGBUILD is not valid shell"
return 1
fi
local dump
if ! dump=$(CARCH=x86_64 bash -c '
source "$1" >/dev/null 2>&1 || exit 1
printf "pkgver\t%s\n" "$pkgver"
printf "pkgrel\t%s\n" "$pkgrel"
for name in "${@:2}"; do
declare -n array="$name"
printf "%s\t%s\n" "$name" "${array[*]}"
done
' _ "$pkgbuild" "${arrays[@]}" 2>/dev/null); then
print_error "Rewritten PKGBUILD could not be read back"
return 1
fi
local expected
expected=$(
printf 'pkgver\t%s\n' "$pkgver"
printf 'pkgrel\t1\n'
local array arch
for array in "${arrays[@]}"; do
arch="${array#sha256sums}"
arch="${arch#_}"
[[ -n "$arch" ]] || arch="any"
printf '%s\t%s\n' "$array" \
"$(jq -r --arg arch "$arch" '.sha256sums[$arch] | join(" ")' <<<"$release")"
done
)
if [[ "$dump" != "$expected" ]]; then
print_error "Rewritten PKGBUILD does not hold the reported release"
diff <(echo "$expected") <(echo "$dump") | sed 's/^/ /' >&2 || true
return 1
fi
}
apply_release() {
local package_dir="$1"
local release="$2"
@@ -173,7 +231,7 @@ apply_release() {
local pkgbuild="$package_dir/PKGBUILD"
local arch array values
local targets=()
local arrays=()
while IFS= read -r arch; do
if [[ "$arch" == "any" ]]; then
@@ -181,29 +239,47 @@ apply_release() {
else
array="sha256sums_$arch"
fi
targets+=("$arch:$array")
arrays+=("$array")
done < <(jq -r '.sha256sums | keys[]' <<<"$release")
# Everything the update will touch is checked before anything is written. A
# hook naming an array the PKGBUILD does not have must fail with the file
# untouched rather than half rewritten.
assert_single_assignment "$pkgbuild" '^pkgver=' pkgver || return 1
assert_single_assignment "$pkgbuild" '^pkgrel=' pkgrel || return 1
local target
for target in "${targets[@]}"; do
assert_single_assignment "$pkgbuild" "^${target#*:}=(" "${target#*:}" || return 1
done
# validate_release guarantees at least one entry, so an empty list here means
# jq died inside the process substitution rather than that there is nothing
# to do.
if [[ ${#arrays[@]} -eq 0 ]]; then
print_error "Could not read the checksum architectures from the reported release"
return 1
fi
for target in "${targets[@]}"; do
arch="${target%%:*}"
array="${target#*:}"
# Every edit lands on a scratch copy that replaces the PKGBUILD in one rename
# at the end, so a failure part way through leaves the original untouched
# rather than half updated.
local scratch="$pkgbuild.sync-upstream"
cp "$pkgbuild" "$scratch" || return 1
mapfile -t values < <(jq -r --arg arch "$arch" '.sha256sums[$arch][]' <<<"$release")
set_pkgbuild_array "$pkgbuild" "$array" "${values[@]}" || return 1
done
if ! (
assert_single_assignment "$scratch" '^pkgver=' pkgver || exit 1
assert_single_assignment "$scratch" '^pkgrel=' pkgrel || exit 1
set_pkgbuild_scalar "$pkgbuild" pkgver "$pkgver" || return 1
set_pkgbuild_scalar "$pkgbuild" pkgrel 1 || return 1
for array in "${arrays[@]}"; do
arch="${array#sha256sums}"
arch="${arch#_}"
[[ -n "$arch" ]] || arch="any"
mapfile -t values < <(jq -r --arg arch "$arch" '.sha256sums[$arch][]' <<<"$release")
set_pkgbuild_array "$scratch" "$array" "${values[@]}" || exit 1
done
set_pkgbuild_scalar "$scratch" pkgver "$pkgver" || exit 1
set_pkgbuild_scalar "$scratch" pkgrel 1 || exit 1
verify_pkgbuild "$scratch" "$release" "$pkgver" "${arrays[@]}" || exit 1
); then
rm -f "$scratch"
return 1
fi
chmod --reference="$pkgbuild" "$scratch"
mv "$scratch" "$pkgbuild"
}
sync_package() {
@@ -9,17 +9,28 @@ set -euo pipefail
BASE_URL="https://persistent.oaistatic.com/codex-app-prod/linux/deb"
declare -A DEB_ARCHES=([x86_64]=amd64 [aarch64]=arm64)
# Print "<version> <sha256>" for the newest stanza in a Packages index.
# Print "<version> <sha256>" for the newest stanza in a Packages index. Newest
# is vercmp's opinion, which is the one bin/sync-upstream and pacman both use;
# sort -V disagrees with it over versions like 1.0a.
newest_release() {
local index="$1"
local version sha256 best_version="" best_sha256=""
awk '
while read -r version sha256; do
if [[ -z "$best_version" ]] || [[ "$(vercmp "$version" "$best_version")" -gt 0 ]]; then
best_version="$version"
best_sha256="$sha256"
fi
done < <(awk '
{ sub(/\r$/, "") }
/^Version:/ { version = $2 }
/^SHA256:/ { sha256 = $2 }
/^$/ { if (version && sha256) print version, sha256; version = sha256 = "" }
END { if (version && sha256) print version, sha256 }
' <<<"$index" | sort -V | tail -n 1
' <<<"$index")
[[ -n "$best_version" ]] || return 1
echo "$best_version $best_sha256"
}
versions=()
+1 -1
View File
@@ -70,7 +70,7 @@ _pool="https://persistent.oaistatic.com/codex-app-prod/linux/deb/pool/main/c/cha
source_x86_64=("${_deb_x86_64}::${_pool}/${_deb_x86_64}")
source_aarch64=("${_deb_aarch64}::${_pool}/${_deb_aarch64}")
noextract=("${_deb_x86_64}" "${_deb_aarch64}")
sha256sums=('fc70527cd961f3a660e2a9d0a2d62b1e3f7a61d8482ee1935a6b25307da278eb')
sha256sums=('b3a4503b5931f102444bc7015c3cf4e40266cf034e0d682bd2a407dc5b3ee58c')
sha256sums_x86_64=('708a15a1bb76e2bb7f0e376e5145391fa277ad3a64057c1d32537bdc2a1b4e6e')
sha256sums_aarch64=('6ebea681b1e494d218a199f638b4bc886e94e1458dd61079b1e390a6fb98fdd2')
@@ -2,9 +2,11 @@
set -euo pipefail
user_flags=()
flags_file="${XDG_CONFIG_HOME:-${HOME:-}/.config}/codex-flags.conf"
config_home="${XDG_CONFIG_HOME:-}"
[[ -n "$config_home" || -z "${HOME:-}" ]] || config_home="$HOME/.config"
flags_file="${config_home:+$config_home/codex-flags.conf}"
if [[ -r "$flags_file" ]]; then
if [[ -n "$flags_file" && -f "$flags_file" && -r "$flags_file" ]]; then
while IFS= read -r line || [[ -n "$line" ]]; do
line="${line%%#*}"
[[ -n "${line//[[:space:]]/}" ]] || continue