From 01a566f01adba272a151b53bb3e50e053e6ca91d Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 15 Aug 2026 08:18:25 -0700 Subject: [PATCH 1/3] Add bin/sync-upstream for packages that track a vendor release feed Some vendors publish a release feed of their own that is faster and more precise than anyone's packaging of it. A package opts in with an .omarchy/upstream.sh hook that reports the newest release as JSON, and the driver rewrites pkgver, the checksum arrays the hook names, and pkgrel. Writes are guarded on both ends: every assignment the update will touch is verified to exist before anything is written, so a hook naming an array the PKGBUILD lacks fails with the file untouched rather than half rewritten; and pkgver is held to pacman's character set, because it lands in a file makepkg sources as shell. Ordering is vercmp's, not sort -V's -- they disagree about whether 1.0a precedes 1.0, and pacman is what decides if a published package is an upgrade. That is also why the workflow runs in an Arch container rather than straight on the runner. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/sync-upstream.yml | 95 +++++++++ README.md | 42 +++- bin/sync-upstream | 314 ++++++++++++++++++++++++++++ helpers/package-metadata.sh | 13 ++ 4 files changed, 462 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/sync-upstream.yml create mode 100755 bin/sync-upstream diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml new file mode 100644 index 0000000..0083ec3 --- /dev/null +++ b/.github/workflows/sync-upstream.yml @@ -0,0 +1,95 @@ +name: Sync Upstream Releases + +on: + schedule: + # Every 6 hours, off the hour to dodge the scheduling backlog at :00 + - cron: '20 */6 * * *' + workflow_dispatch: + inputs: + packages: + description: 'Specific packages to update (space-separated, leave empty for all)' + required: false + default: '' + +jobs: + sync: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + # Runs in an Arch container for vercmp: whether a release is an upgrade has + # to be decided by the same comparator pacman will use on users' machines. + - name: Update packages from upstream release feeds + run: | + docker run --rm \ + -e PACKAGES="$PACKAGES" \ + -e HOST_UID="$(id -u)" \ + -e HOST_GID="$(id -g)" \ + -v "$PWD/bin:/workspace/bin:ro" \ + -v "$PWD/helpers:/workspace/helpers:ro" \ + -v "$PWD/pkgbuilds:/workspace/pkgbuilds" \ + -w /workspace \ + archlinux:base-devel bash -lc ' + set -euo pipefail + + pacman -Syu --noconfirm jq + + groupadd -g "$HOST_GID" runner + useradd -m -u "$HOST_UID" -g "$HOST_GID" runner + chown -R runner:runner /workspace/pkgbuilds + + if [[ -n "${PACKAGES:-}" ]]; then + read -r -a package_args <<< "$PACKAGES" + runuser -u runner -- ./bin/sync-upstream "${package_args[@]}" + else + runuser -u runner -- ./bin/sync-upstream + fi + ' + env: + PACKAGES: ${{ github.event.inputs.packages }} + + - name: Check for changes + id: changes + run: | + if [ -z "$(git status --porcelain)" ]; then + echo "has_changes=false" >> "$GITHUB_OUTPUT" + else + echo "has_changes=true" >> "$GITHUB_OUTPUT" + fi + + - name: Create Pull Request + if: steps.changes.outputs.has_changes == 'true' + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: 'chore: sync upstream releases' + title: 'chore: sync upstream releases' + body: | + Automated update of packages that track an upstream vendor release + feed rather than the AUR. + + Each package reports its newest release through + `.omarchy/upstream.sh`. + branch: auto/sync-upstream + delete-branch: true + labels: automated + reviewers: ryanrhughes + + - name: Notify Basecamp on failure + if: failure() && env.BASECAMP_CHATBOT_URL != '' + env: + BASECAMP_CHATBOT_URL: ${{ secrets.BASECAMP_CHATBOT_URL }} + run: | + curl -s -o /dev/null \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg content \ + "πŸ”΄ Upstream sync failed
View run" \ + '{content: $content}')" \ + "$BASECAMP_CHATBOT_URL" diff --git a/README.md b/README.md index 5f1e891..50c03f0 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ The filesystem no longer encodes release policy. Instead: - all other packages reach `stable` by promoting tested edge artifacts with `bin/repo migrate` - AUR sync behavior is controlled by `source`, `sync`, `aur`, patches, and hooks in `.omarchy/` - packages can opt out of unscoped builds with `skip_build`; explicit `--package` builds remain available +- packages that follow a vendor release feed instead of the AUR carry an `.omarchy/upstream.sh` hook ## Prerequisites ### aarch64 Builds (Optional) @@ -248,6 +249,40 @@ bin/sync-aur yay v4l2-relayd # Sync specific packages AUR sync is metadata-driven. It preserves `.omarchy/`, replaces the package root with AUR contents, applies `.omarchy/patches/*.patch`, runs `.omarchy/post-sync.sh` when present, applies pkgrel metadata, removes AUR-only `.SRCINFO` and `.gitignore` files, and records `upstream_commit`. +### Sync Upstream Releases + +```bash +bin/sync-upstream # Update every package with an upstream hook +bin/sync-upstream openai-codex-desktop # Update specific packages +``` + +Some vendors publish a release feed of their own that is faster and more precise +than the AUR packaging of it. Those packages are `source: local` β€” Omarchy owns +the PKGBUILD β€” and provide `.omarchy/upstream.sh`, a hook that reports the newest +upstream release as JSON on stdout: + +```json +{ + "pkgver": "1.2.3", + "sha256sums": { "x86_64": [""], "aarch64": [""] } +} +``` + +Architecture keys become `sha256sums_` in the PKGBUILD; the key `any` means +the unsuffixed `sha256sums` array, and only the arrays a hook names are touched. +An empty object (`{}`) reports no update, which is how a hook waits out a release +that has landed for one architecture but not yet the other. + +When the reported version is newer than the checked-in one, `bin/sync-upstream` +rewrites `pkgver` and those checksum arrays and resets `pkgrel` to 1. A version +that is equal or older leaves the package alone, so a vendor rolling a release +back cannot walk the repository backwards. + +Hooks should read checksums from whatever manifest the vendor publishes rather +than downloading the artifacts β€” see `pkgbuilds/openai-codex-desktop/.omarchy/upstream.sh`, +which reads OpenAI's Debian package index and never fetches the 750 MB of debs +it describes. + ### Other ```bash @@ -260,6 +295,7 @@ bin/repo push # Upload local builds to the host and publi bin/add-package # Add an AUR/local package with metadata bin/package-worktree # Create upstream/patched/current scratch workspace bin/repo remove # Remove package +bin/sync-upstream # Update packages that track a vendor release feed bin/clean-docker # Clear Docker images/cache (forces fresh rebuild) ``` @@ -345,7 +381,8 @@ omarchy-pkgs/ β”‚ └── .omarchy/ β”‚ β”œβ”€β”€ package.json # Source/sync/release metadata β”‚ β”œβ”€β”€ patches/ # Omarchy patches reapplied after AUR sync -β”‚ └── post-sync.sh # Optional dynamic post-sync customization hook +β”‚ β”œβ”€β”€ post-sync.sh # Optional dynamic post-sync customization hook +β”‚ └── upstream.sh # Optional vendor release feed hook (non-AUR packages) β”œβ”€β”€ build/ β”œβ”€β”€ build-output/ # Unsigned packages (temporary) β”‚ β”œβ”€β”€ edge/ @@ -396,7 +433,7 @@ Minimal examples: Fields: -- `source`: `aur` or `local` +- `source`: `aur` or `local`. A `local` package can still follow an upstream release with an `.omarchy/upstream.sh` hook. - `sync`: optional for AUR packages; defaults to `true`. Set `false` for AUR-origin packages that Omarchy maintains manually. - `aur`: optional AUR package name when it differs from the local package directory, usually for split packages. - `release_ring`: optional. `fast` means the package is built directly for stable as well as edge. Packages without a ring build in edge and reach stable through tested artifact promotion (`bin/repo migrate`). @@ -539,6 +576,7 @@ The repository includes GitHub workflows and systemd services for automated rele #### GitHub Workflows 1. **sync-aur.yml** (Every 6 hours): Syncs AUR packages according to `.omarchy/package.json` and opens a PR when changes are found. +2. **sync-upstream.yml** (Every 6 hours): Runs `.omarchy/upstream.sh` for packages that track a vendor release feed and opens a PR when a newer version is out. #### Systemd Services diff --git a/bin/sync-upstream b/bin/sync-upstream new file mode 100755 index 0000000..c307cf8 --- /dev/null +++ b/bin/sync-upstream @@ -0,0 +1,314 @@ +#!/bin/bash +set -euo pipefail + +BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..") +source "$BUILD_ROOT/helpers/message-helpers.sh" +source "$BUILD_ROOT/helpers/paths.sh" +source "$BUILD_ROOT/helpers/package-metadata.sh" + +TEMP_DIR=$(mktemp -d) +trap 'rm -rf "$TEMP_DIR"' EXIT + +SPECIFIC_PACKAGES=() + +usage() { + cat </.omarchy/upstream.sh, a hook +that reports the newest upstream release as JSON on stdout: + + { + "pkgver": "1.2.3", + "sha256sums": { "x86_64": [""], "aarch64": [""] } + } + +Architecture keys become sha256sums_ in the PKGBUILD; the key "any" means +the unsuffixed sha256sums array. An empty object ({}) reports no update. + +When the reported version is newer than the checked-in one, pkgver and the +listed checksum arrays are rewritten and pkgrel is reset to 1. + +Arguments: + PACKAGE One or more package names to update (optional) + +Examples: + $0 # Update every package with an upstream hook + $0 openai-codex-desktop # Update specific packages +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + usage + exit 0 + ;; + --*) + print_error "Unknown option: $1" + exit 1 + ;; + *) + SPECIFIC_PACKAGES+=("$1") + shift + ;; + esac +done + +if ! command -v vercmp >/dev/null 2>&1; then + print_error "vercmp not found: this needs pacman to decide whether a release is an upgrade" + exit 1 +fi + +print_header "Upstream Package Sync" + +UPDATED=0 +SKIPPED=0 +FAILED=0 +SPECIFIC_MODE=false + +get_pkgver() { + local package_dir="$1" + + grep -m1 '^pkgver=' "$package_dir/PKGBUILD" | cut -d= -f2- | tr -d "\"'" +} + +assert_single_assignment() { + local pkgbuild="$1" + local pattern="$2" + local label="$3" + + if [[ $(grep -c "$pattern" "$pkgbuild") -ne 1 ]]; then + print_error "Expected exactly one $label assignment in $pkgbuild" + return 1 + fi +} + +set_pkgbuild_scalar() { + local pkgbuild="$1" + local field="$2" + local value="$3" + + assert_single_assignment "$pkgbuild" "^${field}=" "$field" || return 1 + sed -i "s/^${field}=.*/${field}=${value}/" "$pkgbuild" +} + +# Replace an array assignment, however many lines the original spans. +set_pkgbuild_array() { + local pkgbuild="$1" + local name="$2" + shift 2 + local values=("$@") + + 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" + fi + + # Rewrite beside the PKGBUILD so the move is an atomic same-filesystem rename. + local rewritten="$pkgbuild.sync-upstream" + if ! awk -v prefix="${name}=(" -v block="$block" ' + !replaced && index($0, prefix) == 1 { + while ((getline line < block) > 0) print line + close(block) + replaced = 1 + # A ")" anywhere past the opening closes the array; testing for one at end + # of line instead would treat a trailing comment as a continuation and eat + # every line up to the next ")". + if (index(substr($0, length(prefix) + 1), ")") == 0) skipping = 1 + next + } + skipping { if ($0 ~ /\)/) skipping = 0; next } + { print } + ' "$pkgbuild" > "$rewritten"; then + print_error "Failed to rewrite ${name} in $pkgbuild" + rm -f "$rewritten" + return 1 + fi + + chmod --reference="$pkgbuild" "$rewritten" + mv "$rewritten" "$pkgbuild" +} + +# pacman's own comparator, because nothing else agrees with it at the corners: +# sort -V calls 1.0a newer than 1.0, vercmp calls it older, and pacman is what +# decides whether a published package is an upgrade. +version_is_newer() { + local candidate="$1" + local current="$2" + + [[ "$candidate" != "$current" ]] || return 1 + [[ "$(vercmp "$candidate" "$current")" -gt 0 ]] +} + +validate_release() { + local release="$1" + + # 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. + jq -e ' + (.pkgver | type == "string" and test("^[A-Za-z0-9._+]+$")) + and (.sha256sums | type == "object" and length > 0) + and (.sha256sums | to_entries | all( + .key | test("^[a-z0-9_]+$") + )) + and (.sha256sums | to_entries | all( + .value | type == "array" and length > 0 and all(test("^[0-9a-f]{64}$")) + )) + ' <<<"$release" >/dev/null +} + +apply_release() { + local package_dir="$1" + local release="$2" + local pkgver="$3" + local pkgbuild="$package_dir/PKGBUILD" + + local arch array values + local targets=() + + while IFS= read -r arch; do + if [[ "$arch" == "any" ]]; then + array="sha256sums" + else + array="sha256sums_$arch" + fi + targets+=("$arch:$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 + + for target in "${targets[@]}"; do + arch="${target%%:*}" + array="${target#*:}" + + mapfile -t values < <(jq -r --arg arch "$arch" '.sha256sums[$arch][]' <<<"$release") + set_pkgbuild_array "$pkgbuild" "$array" "${values[@]}" || return 1 + done + + set_pkgbuild_scalar "$pkgbuild" pkgver "$pkgver" || return 1 + set_pkgbuild_scalar "$pkgbuild" pkgrel 1 || return 1 +} + +sync_package() { + local package="$1" + local package_dir="$PKGBUILDS_DIR/$package" + local hook="$package_dir/.omarchy/upstream.sh" + + if [[ ! -f "$package_dir/PKGBUILD" ]]; then + print_error "Package $package has no PKGBUILD" + ((++FAILED)) + return 0 + fi + + if [[ ! -f "$hook" ]]; then + if [[ "$SPECIFIC_MODE" == true ]]; then + print_error "Package $package is missing .omarchy/upstream.sh" + ((++FAILED)) + else + print_info "Skipping $package: no upstream hook" + ((++SKIPPED)) + fi + return 0 + fi + + print_info "Checking $package for upstream releases..." + + local release + if ! release=$(cd "$package_dir" && PACKAGE_NAME="$package" bash .omarchy/upstream.sh); then + print_error "Upstream hook failed for $package" + ((++FAILED)) + return 0 + fi + + if ! jq -e . >/dev/null 2>&1 <<<"$release"; then + print_error "Upstream hook for $package did not report valid JSON" + ((++FAILED)) + return 0 + fi + + if [[ "$(jq -r 'has("pkgver")' <<<"$release")" == "false" ]]; then + print_info " No upstream update reported" + ((++SKIPPED)) + return 0 + fi + + if ! validate_release "$release"; then + print_error "Upstream hook for $package reported a malformed release" + ((++FAILED)) + return 0 + fi + + local pkgver current_pkgver + pkgver=$(jq -r '.pkgver' <<<"$release") + current_pkgver=$(get_pkgver "$package_dir") + + if [[ -z "$current_pkgver" ]]; then + print_error "Could not read pkgver from $package_dir/PKGBUILD" + ((++FAILED)) + return 0 + fi + + if [[ "$pkgver" == "$current_pkgver" ]]; then + print_info " Already at $current_pkgver" + ((++SKIPPED)) + return 0 + fi + + if ! version_is_newer "$pkgver" "$current_pkgver"; then + print_warning " Upstream reports $pkgver, older than the checked-in $current_pkgver; leaving it alone" + ((++SKIPPED)) + return 0 + fi + + if ! apply_release "$package_dir" "$release" "$pkgver"; then + print_error "Failed to update $package" + ((++FAILED)) + return 0 + fi + + print_success " $current_pkgver -> $pkgver" + ((++UPDATED)) +} + +if [[ ${#SPECIFIC_PACKAGES[@]} -gt 0 ]]; then + SPECIFIC_MODE=true + for package in "${SPECIFIC_PACKAGES[@]}"; do + sync_package "$package" + done +else + while IFS= read -r package; do + sync_package "$package" + done < <(packages_for_upstream_sync) +fi + +echo "" +if [[ $FAILED -gt 0 ]]; then + print_error "Upstream sync completed with failures" +else + print_success "Upstream sync complete!" +fi +echo " Target: $PKGBUILDS_DIR" +echo " Updated: $UPDATED" +echo " Skipped: $SKIPPED" +echo " Failed: $FAILED" + +if [[ $FAILED -gt 0 ]]; then + exit 1 +fi diff --git a/helpers/package-metadata.sh b/helpers/package-metadata.sh index 1160fff..b1f1b70 100644 --- a/helpers/package-metadata.sh +++ b/helpers/package-metadata.sh @@ -135,6 +135,19 @@ packages_for_aur_sync() { done } +package_has_upstream_hook() { + local pkgdir="$1" + [[ -f "$pkgdir/.omarchy/upstream.sh" ]] +} + +packages_for_upstream_sync() { + package_dirs | while IFS= read -r pkgdir; do + if package_has_upstream_hook "$pkgdir"; then + basename "$pkgdir" + fi + done +} + packages_for_mirror() { local mirror="$1" From 1a61278911915dc261461fc7bfb638ab3efd57e1 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 15 Aug 2026 08:18:35 -0700 Subject: [PATCH 2/3] Take over openai-codex-desktop from the AUR OpenAI ships the ChatGPT desktop app several times a week and the AUR packaging trails it -- as of this commit by a full version, 26.803.81509 against 26.810.52044. Every sync we took from there was a sync we could have taken from OpenAI directly. So track OpenAI's own Debian repository instead. Its per-architecture package index carries the version and SHA256 of every deb, which makes an update two small HTTP requests rather than a 750 MB download, and the pool keeps old versions, so the URLs pinned here stay resolvable after the next release. Omarchy now maintains the package outright: the max-zstd patch is simply part of the PKGBUILD, chatgpt-launcher.sh is ours, and the Arch REUSE files are gone -- they annotated packaging paths (.SRCINFO, keys/**, .nvchecker.toml) that do not exist here. The app's own license still ships; package() installs upstream's copyright file. Co-Authored-By: Claude Opus 5 (1M context) --- .../.omarchy/package.json | 5 +- .../patches/max-zstd-compression.patch | 15 ----- .../openai-codex-desktop/.omarchy/upstream.sh | 55 +++++++++++++++++++ pkgbuilds/openai-codex-desktop/LICENSE | 12 ---- .../openai-codex-desktop/LICENSES/0BSD.txt | 1 - pkgbuilds/openai-codex-desktop/PKGBUILD | 27 +++++---- pkgbuilds/openai-codex-desktop/REUSE.toml | 25 --------- .../openai-codex-desktop/chatgpt-launcher.sh | 49 +++++++---------- 8 files changed, 91 insertions(+), 98 deletions(-) delete mode 100644 pkgbuilds/openai-codex-desktop/.omarchy/patches/max-zstd-compression.patch create mode 100755 pkgbuilds/openai-codex-desktop/.omarchy/upstream.sh delete mode 100644 pkgbuilds/openai-codex-desktop/LICENSE delete mode 120000 pkgbuilds/openai-codex-desktop/LICENSES/0BSD.txt delete mode 100644 pkgbuilds/openai-codex-desktop/REUSE.toml mode change 100644 => 100755 pkgbuilds/openai-codex-desktop/chatgpt-launcher.sh diff --git a/pkgbuilds/openai-codex-desktop/.omarchy/package.json b/pkgbuilds/openai-codex-desktop/.omarchy/package.json index 8d7f14b..db153c3 100644 --- a/pkgbuilds/openai-codex-desktop/.omarchy/package.json +++ b/pkgbuilds/openai-codex-desktop/.omarchy/package.json @@ -1,5 +1,4 @@ { - "source": "aur", - "release_ring": "fast", - "upstream_commit": "05f12e3c51ce07efb996698d4fae94327f45f03b" + "source": "local", + "release_ring": "fast" } diff --git a/pkgbuilds/openai-codex-desktop/.omarchy/patches/max-zstd-compression.patch b/pkgbuilds/openai-codex-desktop/.omarchy/patches/max-zstd-compression.patch deleted file mode 100644 index 58bd5a8..0000000 --- a/pkgbuilds/openai-codex-desktop/.omarchy/patches/max-zstd-compression.patch +++ /dev/null @@ -1,15 +0,0 @@ ---- a/PKGBUILD -+++ b/PKGBUILD -@@ -55,6 +55,12 @@ - conflicts=('chatgpt') - options=('!debug' '!strip') - -+# Omarchy: the build image compresses with zstd at its default level, which -+# leaves this 1.3 GB Electron tree at 447 MB. Maximum zstd takes it to 322 MB -+# for ~4 extra minutes of build time -- worth it for a package this large that -+# every user re-downloads on each of OpenAI's frequent releases. -+COMPRESSZST=(zstd -c -z -q --ultra -22 --threads=0 -) -+ - _deb_x86_64="chatgpt_${pkgver}_amd64.deb" - _deb_aarch64="chatgpt_${pkgver}_arm64.deb" - source=('chatgpt-launcher.sh') diff --git a/pkgbuilds/openai-codex-desktop/.omarchy/upstream.sh b/pkgbuilds/openai-codex-desktop/.omarchy/upstream.sh new file mode 100755 index 0000000..ee749e3 --- /dev/null +++ b/pkgbuilds/openai-codex-desktop/.omarchy/upstream.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# OpenAI ships the ChatGPT desktop app from its own Debian repository. The +# per-architecture package index carries both the version and the SHA256 of +# every deb, so an update costs two small HTTP requests instead of a 750 MB +# download, and the pool keeps old versions, so the URLs pinned in the PKGBUILD +# stay resolvable after the next release. +set -euo pipefail + +BASE_URL="https://persistent.oaistatic.com/codex-app-prod/linux/deb" +declare -A DEB_ARCHES=([x86_64]=amd64 [aarch64]=arm64) + +# Print " " for the newest stanza in a Packages index. +newest_release() { + local index="$1" + + 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 +} + +versions=() +declare -A checksums=() + +for arch in "${!DEB_ARCHES[@]}"; do + index=$(curl -fsSL "$BASE_URL/dists/stable/main/binary-${DEB_ARCHES[$arch]}/Packages") + + read -r version sha256 <<<"$(newest_release "$index")" + if [[ -z "${version:-}" || -z "${sha256:-}" ]]; then + echo "No usable release found for $arch in the upstream package index" >&2 + exit 1 + fi + + versions+=("$version") + checksums[$arch]="$sha256" +done + +# A release lands one architecture at a time, and a single pkgver has to cover +# both. Report no update until they agree; the next run picks it up. +for version in "${versions[@]}"; do + if [[ "$version" != "${versions[0]}" ]]; then + echo "Upstream architectures are mid-release (${versions[*]}); skipping" >&2 + echo '{}' + exit 0 + fi +done + +jq -n \ + --arg pkgver "${versions[0]}" \ + --arg x86_64 "${checksums[x86_64]}" \ + --arg aarch64 "${checksums[aarch64]}" \ + '{pkgver: $pkgver, sha256sums: {x86_64: [$x86_64], aarch64: [$aarch64]}}' diff --git a/pkgbuilds/openai-codex-desktop/LICENSE b/pkgbuilds/openai-codex-desktop/LICENSE deleted file mode 100644 index b87c5e4..0000000 --- a/pkgbuilds/openai-codex-desktop/LICENSE +++ /dev/null @@ -1,12 +0,0 @@ -Copyright Arch Linux Contributors - -Permission to use, copy, modify, and/or distribute this software for -any purpose with or without fee is hereby granted. - -THE SOFTWARE IS PROVIDED β€œAS IS” AND THE AUTHOR DISCLAIMS ALL -WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE -FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY -DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN -AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT -OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/pkgbuilds/openai-codex-desktop/LICENSES/0BSD.txt b/pkgbuilds/openai-codex-desktop/LICENSES/0BSD.txt deleted file mode 120000 index ea5b606..0000000 --- a/pkgbuilds/openai-codex-desktop/LICENSES/0BSD.txt +++ /dev/null @@ -1 +0,0 @@ -../LICENSE \ No newline at end of file diff --git a/pkgbuilds/openai-codex-desktop/PKGBUILD b/pkgbuilds/openai-codex-desktop/PKGBUILD index 52393e7..8daed49 100644 --- a/pkgbuilds/openai-codex-desktop/PKGBUILD +++ b/pkgbuilds/openai-codex-desktop/PKGBUILD @@ -1,10 +1,12 @@ -# Maintainer: Parsiad Azimzadeh -# Maintainer: mothran -# Official downloads: https://chatgpt.com/codex/ +# Maintainer: David Heinemeier Hansson + +# Omarchy tracks OpenAI's own Debian repository rather than the AUR, which lags +# behind releases that ship several times a week. .omarchy/upstream.sh rewrites +# the version and checksums below from that repository's package index. pkgname=openai-codex-desktop -pkgver=26.803.81509 -pkgrel=8.1 +pkgver=26.810.52044 +pkgrel=1 pkgdesc="Official ChatGPT desktop app with Codex" arch=('x86_64' 'aarch64') url="https://chatgpt.com/codex/" @@ -64,16 +66,13 @@ COMPRESSZST=(zstd -c -z -q --ultra -22 --threads=0 -) _deb_x86_64="chatgpt_${pkgver}_amd64.deb" _deb_aarch64="chatgpt_${pkgver}_arm64.deb" source=('chatgpt-launcher.sh') -source_x86_64=( - "${_deb_x86_64}::https://persistent.oaistatic.com/codex-app-prod/linux/deb/latest/chatgpt_amd64.deb" -) -source_aarch64=( - "${_deb_aarch64}::https://persistent.oaistatic.com/codex-app-prod/linux/deb/latest/chatgpt_arm64.deb" -) +_pool="https://persistent.oaistatic.com/codex-app-prod/linux/deb/pool/main/c/chatgpt" +source_x86_64=("${_deb_x86_64}::${_pool}/${_deb_x86_64}") +source_aarch64=("${_deb_aarch64}::${_pool}/${_deb_aarch64}") noextract=("${_deb_x86_64}" "${_deb_aarch64}") -sha256sums=('4e3ca9302600bed268f8fd3ba2c9ac2f1ceb99da139ed71c50db0289b118d06f') -sha256sums_x86_64=('a9bf91a368f9f7c4eea38082a9fb8fb46b8d005b719a6d7715d2e5a1982c38eb') -sha256sums_aarch64=('f38fcc194eca9ab0327dc10c92340681eae77c5d75164df700384ce2adaccbc1') +sha256sums=('fc70527cd961f3a660e2a9d0a2d62b1e3f7a61d8482ee1935a6b25307da278eb') +sha256sums_x86_64=('708a15a1bb76e2bb7f0e376e5145391fa277ad3a64057c1d32537bdc2a1b4e6e') +sha256sums_aarch64=('6ebea681b1e494d218a199f638b4bc886e94e1458dd61079b1e390a6fb98fdd2') package() { cd "${srcdir}" diff --git a/pkgbuilds/openai-codex-desktop/REUSE.toml b/pkgbuilds/openai-codex-desktop/REUSE.toml deleted file mode 100644 index 49e3625..0000000 --- a/pkgbuilds/openai-codex-desktop/REUSE.toml +++ /dev/null @@ -1,25 +0,0 @@ -version = 1 - -[[annotations]] -path = [ - "PKGBUILD", - "README.md", - "keys/**", - ".SRCINFO", - ".gitignore", - ".nvchecker.toml", - "*.install", - "*.sysusers", - "*sysusers.conf", - "*.tmpfiles", - "*tmpfiles.conf", - "*.logrotate", - "*.pam", - "*.service", - "*.socket", - "*.timer", - "*.desktop", - "*.hook", -] -SPDX-FileCopyrightText = "Arch Linux contributors" -SPDX-License-Identifier = "0BSD" diff --git a/pkgbuilds/openai-codex-desktop/chatgpt-launcher.sh b/pkgbuilds/openai-codex-desktop/chatgpt-launcher.sh old mode 100644 new mode 100755 index 0f8bc6b..fa8a099 --- a/pkgbuilds/openai-codex-desktop/chatgpt-launcher.sh +++ b/pkgbuilds/openai-codex-desktop/chatgpt-launcher.sh @@ -1,37 +1,30 @@ #!/bin/bash -# SPDX-FileCopyrightText: 2026 Arch Linux Contributors -# SPDX-License-Identifier: 0BSD set -euo pipefail user_flags=() -ozone_flags=() +flags_file="${XDG_CONFIG_HOME:-${HOME:-}/.config}/codex-flags.conf" -config_home="${XDG_CONFIG_HOME:-}" -if [[ -z "${config_home}" && -n "${HOME:-}" ]]; then - config_home="${HOME}/.config" +if [[ -r "$flags_file" ]]; then + while IFS= read -r line || [[ -n "$line" ]]; do + line="${line%%#*}" + [[ -n "${line//[[:space:]]/}" ]] || continue + read -r -a flags <<<"$line" + user_flags+=("${flags[@]}") + done <"$flags_file" fi -if [[ -n "${config_home}" && -f "${config_home}/codex-flags.conf" ]]; then - while IFS= read -r flag_line || [[ -n "${flag_line}" ]]; do - flag_line="${flag_line%%#*}" - read -r -a flag_parts <<<"${flag_line}" - user_flags+=("${flag_parts[@]}") - done <"${config_home}/codex-flags.conf" +# Chromium's own Ozone detection falls back to XWayland often enough to matter, +# and the result is a blurry window on every scaled display. Ask for Wayland +# directly, unless the user has already picked a platform themselves. +platform_flags=() +if [[ -n "${WAYLAND_DISPLAY:-}" || "${XDG_SESSION_TYPE:-}" == wayland ]]; then + platform_flags=(--ozone-platform=wayland) + + for flag in "${user_flags[@]}" "$@"; do + case "$flag" in + --ozone-platform=* | --ozone-platform-hint=*) platform_flags=() ;; + esac + done fi -# Use native Wayland rendering in Wayland sessions. Chromium's automatic Ozone -# selection can fall back to XWayland, which produces a blurry UI on scaled -# displays. An explicit platform argument from the user always takes precedence. -if [[ "${XDG_SESSION_TYPE:-}" == wayland || -n "${WAYLAND_DISPLAY:-}" ]]; then - ozone_flags=(--ozone-platform=wayland) -fi - -# A platform selected in the flags file or on the command line overrides the -# native Wayland default. -for flag in "${user_flags[@]}" "$@"; do - case "${flag}" in - --ozone-platform=*|--ozone-platform-hint=*) ozone_flags=() ;; - esac -done - -exec /usr/lib/chatgpt/ChatGPT "${ozone_flags[@]}" "${user_flags[@]}" "$@" +exec /usr/lib/chatgpt/ChatGPT "${platform_flags[@]}" "${user_flags[@]}" "$@" From f92de9c44042fad23075ffde65d5619cd5b1e047 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 15 Aug 2026 08:34:42 -0700 Subject: [PATCH 3/3] 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) --- bin/sync-upstream | 140 ++++++++++++++---- .../openai-codex-desktop/.omarchy/upstream.sh | 17 ++- pkgbuilds/openai-codex-desktop/PKGBUILD | 2 +- .../openai-codex-desktop/chatgpt-launcher.sh | 6 +- 4 files changed, 127 insertions(+), 38 deletions(-) diff --git a/bin/sync-upstream b/bin/sync-upstream index c307cf8..93e6f0d 100755 --- a/bin/sync-upstream +++ b/bin/sync-upstream @@ -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() { diff --git a/pkgbuilds/openai-codex-desktop/.omarchy/upstream.sh b/pkgbuilds/openai-codex-desktop/.omarchy/upstream.sh index ee749e3..a090a35 100755 --- a/pkgbuilds/openai-codex-desktop/.omarchy/upstream.sh +++ b/pkgbuilds/openai-codex-desktop/.omarchy/upstream.sh @@ -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 " " for the newest stanza in a Packages index. +# Print " " 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=() diff --git a/pkgbuilds/openai-codex-desktop/PKGBUILD b/pkgbuilds/openai-codex-desktop/PKGBUILD index 8daed49..a92ee95 100644 --- a/pkgbuilds/openai-codex-desktop/PKGBUILD +++ b/pkgbuilds/openai-codex-desktop/PKGBUILD @@ -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') diff --git a/pkgbuilds/openai-codex-desktop/chatgpt-launcher.sh b/pkgbuilds/openai-codex-desktop/chatgpt-launcher.sh index fa8a099..2c4f4f2 100755 --- a/pkgbuilds/openai-codex-desktop/chatgpt-launcher.sh +++ b/pkgbuilds/openai-codex-desktop/chatgpt-launcher.sh @@ -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