diff --git a/bin/sync-upstream b/bin/sync-upstream index 8b2864d..148cd13 100755 --- a/bin/sync-upstream +++ b/bin/sync-upstream @@ -971,6 +971,10 @@ EOF check "malformed upstream fails the sync instead of skipping" "1" "$((FAILED - prev_failed))" FAILED=0 + if ! bash "$BUILD_ROOT/pkgbuilds/cua-driver-bin/.omarchy/upstream-test.sh"; then + failures=$((failures + 1)) + fi + echo "" if [[ "$failures" -eq 0 ]]; then print_success "Self-test passed" diff --git a/pkgbuilds/cua-driver-bin/.omarchy/package.json b/pkgbuilds/cua-driver-bin/.omarchy/package.json new file mode 100644 index 0000000..bbe9ae1 --- /dev/null +++ b/pkgbuilds/cua-driver-bin/.omarchy/package.json @@ -0,0 +1,5 @@ +{ + "source": "local", + "release_ring": "fast", + "min_release_age": "24h" +} diff --git a/pkgbuilds/cua-driver-bin/.omarchy/upstream-test.sh b/pkgbuilds/cua-driver-bin/.omarchy/upstream-test.sh new file mode 100644 index 0000000..dbb6f1a --- /dev/null +++ b/pkgbuilds/cua-driver-bin/.omarchy/upstream-test.sh @@ -0,0 +1,160 @@ +#!/bin/bash +# Offline hook fixtures, also run by bin/sync-upstream self-test. Requires +# GNU date, jq, and pacman's vercmp, like the repository's other self-tests. +set -euo pipefail +hook="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/upstream.sh" +fixture_dir=$(mktemp -d) +trap 'rm -rf "$fixture_dir"' EXIT +printf 'pkgver=0.23.2\n' > "$fixture_dir/PKGBUILD" +cd "$fixture_dir" + +failures=0 +check() { + if [[ "$2" == "$3" ]]; then + echo " ok: $1" + else + echo " FAIL: $1 (expected '$2', got '$3')" + failures=$((failures + 1)) + fi +} + +# Freeze time at the exact 24-hour boundary of the fixture's 0.24.0. +date() { + if [[ "$*" == '+%s' ]]; then + jq -nr '"2026-09-08T16:51:50Z" | fromdateiso8601' + else + command date "$@" + fi +} +curl() { + local url="${!#}" + case "$url" in + 'https://api.github.com/repos/trycua/cua/releases?per_page=100&page='*) + local page="${url##*=}" + [[ "$page" != "${FAIL_PAGE:-}" ]] || { echo 'fixture API failure' >&2; return 22; } + if [[ "${ENDLESS:-}" == 1 || "$page" == 1 ]]; then + if [[ "${LARGE_PAGE:-}" == 1 ]]; then + jq -c '.[0].body = ("x" * 131072)' <<<"$PAGE1" + else + printf '%s\n' "$PAGE1" + fi + elif [[ "$page" == 2 ]]; then + printf '%s\n' "$PAGE2" + else + echo '[]' + fi + ;; + 'https://github.com/trycua/cua/releases/download/'*'/checksums.txt') + [[ "${FAIL_CHECKSUMS:-}" != 1 ]] || { echo 'fixture download failure' >&2; return 22; } + printf '%s\n' "$CHECKSUMS" + ;; + *) echo "unexpected fixture URL: $url" >&2; return 1 ;; + esac +} +export -f curl date +export PAGE1 PAGE2 CHECKSUMS FAIL_PAGE FAIL_CHECKSUMS ENDLESS LARGE_PAGE +export MIN_RELEASE_AGE_SECONDS=86400 +export BYPASS_MIN_RELEASE_AGE='' +FAIL_PAGE='' FAIL_CHECKSUMS='' ENDLESS='' LARGE_PAGE='' +PAGE2='[]' + +release() { + jq -cn --arg tag "$1" --arg published "${2:-2026-09-06T16:51:50Z}" \ + '{tag_name: $tag, published_at: $published, draft: false, prerelease: true}' +} +run_hook() { + status=0 + out=$(bash "$hook" 2>&1) || status=$? +} +expect_failure() { + run_hook + check "$1 fails" yes "$([[ "$status" != 0 ]] && echo yes || echo no)" + check "$1 reports the reason" yes "$([[ "$out" == *"$2"* ]] && echo yes || echo no)" +} +sum_x=$(printf 'a%.0s' {1..64}) +sum_a=$(printf 'b%.0s' {1..64}) +CHECKSUMS=$(printf '%s\n' \ + "$sum_x cua-driver-rs-0.24.0-linux-x86_64.tar.gz" \ + "$sum_a cua-driver-rs-0.24.0-linux-arm64.tar.gz") +foreign_page=$(jq -cn '[range(100) | {tag_name: "fleet-v9.0.0", draft: false}]') +stable=$(release cua-driver-rs-v0.24.0 2026-09-07T16:51:50Z) + +echo 'Cua Driver release hook:' +PAGE1="$foreign_page" +PAGE2="[$stable]" +run_hook +check 'component found beyond the first 100 releases' 0 "$status" +check 'stable-shaped prerelease accepted at exactly 24h' 0.24.0 "$(jq -r '.pkgver' <<<"$out")" +check 'x86_64 checksum' "$sum_x" "$(jq -r '.sha256sums.x86_64[0]' <<<"$out")" +check 'ARM checksum' "$sum_a" "$(jq -r '.sha256sums.aarch64[0]' <<<"$out")" +check 'publication date preserved' 2026-09-07T16:51:50Z "$(jq -r '.published_at' <<<"$out")" + +LARGE_PAGE=1 +run_hook +check 'release page larger than the argument limit succeeds' 0 "$status" +check 'large release page preserves version selection' 0.24.0 "$(jq -r '.pkgver' <<<"$out")" +LARGE_PAGE='' + +# An eligible lower version on page one must not terminate the scan. +rebuilt=$(release cua-driver-rs-v0.23.3 2026-09-07T16:51:50Z) +PAGE1=$(jq -c --argjson rebuilt "$rebuilt" '.[0] = $rebuilt' <<<"$foreign_page") +PAGE2="[$(release cua-driver-rs-v0.24.0)]" +run_hook +check 'older page outranks recently rebuilt lower version' 0.24.0 "$(jq -r '.pkgver' <<<"$out")" + +draft=$(release cua-driver-rs-v99.0.0 | jq -c '.draft = true') +nightly=$(release nightly-cua-driver-rs-v99.0.0) +suffix=$(release cua-driver-rs-v99.0.0-nightly.1) +young=$(release cua-driver-rs-v0.25.0 2026-09-07T16:51:51Z) +PAGE1="[$draft,$nightly,$suffix,$young,$stable,$rebuilt]" +PAGE2='[]' +run_hook +check 'drafts/nightlies rejected and young release quarantined' 0.24.0 "$(jq -r '.pkgver' <<<"$out")" + +PAGE1="[$young]" +run_hook +check 'all candidates too young is a successful skip' 0 "$status" +check 'all young candidates emit empty JSON' '{}' "${out##*$'\n'}" +check 'quarantine reason is reported' yes "$([[ "$out" == *release-age* ]] && echo yes || echo no)" + +PAGE1="[$(release cua-driver-rs-v0.23.2)]" +run_hook +check 'current package version is unchanged' '{}' "$out" + +PAGE1="[$draft,$nightly,$suffix]" +expect_failure 'no stable component candidate' 'no stable cua-driver-rs-v' +PAGE1='[]' +expect_failure 'empty feed' 'no stable cua-driver-rs-v' +PAGE1="[$(release cua-driver-rs-v0.24.0 yesterday)]" +expect_failure 'relative publication date' 'invalid published_at' +PAGE1="[$(release cua-driver-rs-v0.24.0 2026-02-30T00:00:00Z)]" +expect_failure 'impossible publication date' 'invalid published_at' +PAGE1='[{"tag_name":"cua-driver-rs-v0.24.0","draft":false}]' +expect_failure 'missing publication date' 'invalid published_at' + +PAGE1="$foreign_page" +PAGE2='{"message":"API rate limit exceeded"}' +expect_failure 'API error object on later page' 'invalid release feed' +PAGE2='not json' +expect_failure 'malformed JSON on later page' 'invalid release feed' +PAGE2='[42]' +expect_failure 'malformed release entry' 'Cannot index' +PAGE2="[$stable]" +FAIL_PAGE=2 +expect_failure 'network failure after first page' 'fixture API failure' +FAIL_PAGE='' +ENDLESS=1 +expect_failure 'feed exceeds pagination bound' 'refusing incomplete selection' +ENDLESS='' + +PAGE1="[$stable]" +PAGE2='[]' +FAIL_CHECKSUMS=1 +expect_failure 'checksum download failure' 'fixture download failure' +FAIL_CHECKSUMS='' +CHECKSUMS="$sum_x cua-driver-rs-0.24.0-linux-x86_64.tar.gz" +expect_failure 'missing ARM checksum' 'no valid checksum' +CHECKSUMS='invalid cua-driver-rs-0.24.0-linux-x86_64.tar.gz' +expect_failure 'invalid checksum' 'no valid checksum' + +[[ "$failures" == 0 ]] diff --git a/pkgbuilds/cua-driver-bin/.omarchy/upstream.sh b/pkgbuilds/cua-driver-bin/.omarchy/upstream.sh new file mode 100644 index 0000000..e1e1428 --- /dev/null +++ b/pkgbuilds/cua-driver-bin/.omarchy/upstream.sh @@ -0,0 +1,111 @@ +#!/bin/bash +# cua-driver ships from the trycua/cua monorepo, whose single release feed +# interleaves many products (cua-driver-rs-v*, fleet-v*, sandbox-v*, and +# nightly-* builds). The declarative github provider reads a feed as one +# product and stops on the first foreign tag, so this hook selects the newest +# stable cua-driver-rs release itself and reads its checksums.txt manifest. +set -euo pipefail + +REPO="trycua/cua" +TAG_PREFIX="cua-driver-rs-v" + +# Publication order is not version order: older pages can contain a newer +# driver version than a recently rebuilt release. Exhaust the feed before +# selecting, and fail closed if the bounded scan cannot reach its end. +releases='[]' +max_pages=100 +for ((page = 1; page <= max_pages; page++)); do + release_page=$(curl --connect-timeout 10 --max-time 30 -fsSL \ + "https://api.github.com/repos/$REPO/releases?per_page=100&page=$page") + if ! page_size=$(jq -er 'if type == "array" and length <= 100 then length else error("expected release page of at most 100 entries") end' <<<"$release_page"); then + echo "invalid release feed for $REPO on page $page" >&2 + exit 1 + fi + releases=$(printf '%s\n' "$releases" "$release_page" | jq -cs 'add') + (( page_size == 100 )) || break +done +if (( page > max_pages )); then + echo "release feed for $REPO exceeds $max_pages pages; refusing incomplete selection" >&2 + exit 1 +fi + +# Keep jq failures in the main shell, where errexit can reject malformed data. +release_rows=$(jq -r '.[] | select(.draft | not) | [.tag_name // "", .published_at // ""] | @tsv' <<<"$releases") + +min_age="${MIN_RELEASE_AGE_SECONDS:-0}" +now=$(date +%s) +candidates=0 +best_pkgver="" best_tag="" best_published="" +while IFS=$'\t' read -r tag published_at; do + [[ "$tag" == "$TAG_PREFIX"* ]] || continue + pkgver=${tag#"$TAG_PREFIX"} + # Upstream marks every driver release "prerelease" so the monorepo's + # "latest" can point at another product; stability lives in the tag shape + # instead. Stable driver versions are plain dotted numbers -- nightlies + # carry a nightly- tag prefix and a -nightly.N version suffix, and both + # fall out here. + [[ "$pkgver" =~ ^[0-9]+(\.[0-9]+)*$ ]] || continue + + # Strict ISO 8601 before GNU date sees it, matching bin/sync-upstream's + # backstop: date alone also accepts relative expressions, which would let a + # malformed feed fabricate an age instead of failing closed. + if [[ ! "$published_at" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|[+-][0-9]{2}:?[0-9]{2})$ ]] \ + || ! published_epoch=$(date --date="$published_at" +%s 2>/dev/null); then + echo "$REPO release $tag has an invalid published_at: ${published_at:-}" >&2 + exit 1 + fi + candidates=$((candidates + 1)) + + if (( now - published_epoch < min_age )); then + if [[ "${BYPASS_MIN_RELEASE_AGE:-}" == "1" ]]; then + echo "Bypassing release-age gate for $REPO $tag" >&2 + else + continue + fi + fi + + if [[ -z "$best_pkgver" ]] || [[ "$(vercmp "$pkgver" "$best_pkgver")" -gt 0 ]]; then + best_pkgver=$pkgver + best_tag=$tag + best_published=$published_at + fi +done <<<"$release_rows" + +# A feed with no stable driver release at all is an anomaly worth a loud +# error; every candidate merely being inside the quarantine window is not. +if (( candidates == 0 )); then + echo "no stable $TAG_PREFIX releases in the feed for $REPO" >&2 + exit 1 +fi +if [[ -z "$best_tag" ]]; then + echo "every recent $TAG_PREFIX release is still inside the release-age quarantine; skipping" >&2 + echo '{}' + exit 0 +fi + +current=$(grep -m1 '^pkgver=' PKGBUILD | cut -d= -f2- | tr -d "\"'") +if [[ -n "$current" ]] && [[ "$(vercmp "$best_pkgver" "$current")" -le 0 ]]; then + echo '{}' + exit 0 +fi + +checksums=$(curl -fsSL "https://github.com/$REPO/releases/download/$best_tag/checksums.txt") + +sums_json='{}' +for arch in x86_64 aarch64; do + case "$arch" in + x86_64) platform="linux-x86_64" ;; + aarch64) platform="linux-arm64" ;; + esac + asset="cua-driver-rs-${best_pkgver}-${platform}.tar.gz" + sum=$(awk -v f="$asset" '$2 == f { print $1; exit }' <<<"$checksums") + if [[ ! "$sum" =~ ^[0-9a-f]{64}$ ]]; then + echo "no valid checksum for $asset in $REPO $best_tag checksums.txt" >&2 + exit 1 + fi + sums_json=$(jq -c --arg arch "$arch" --arg sum "$sum" '.[$arch] = [$sum]' <<<"$sums_json") +done + +jq -n --arg pkgver "$best_pkgver" --arg published_at "$best_published" \ + --argjson sums "$sums_json" \ + '{pkgver: $pkgver, published_at: $published_at, sha256sums: $sums}' diff --git a/pkgbuilds/cua-driver-bin/LICENSE b/pkgbuilds/cua-driver-bin/LICENSE new file mode 100644 index 0000000..b8b198c --- /dev/null +++ b/pkgbuilds/cua-driver-bin/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Cua AI, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pkgbuilds/cua-driver-bin/PKGBUILD b/pkgbuilds/cua-driver-bin/PKGBUILD new file mode 100644 index 0000000..6af53aa --- /dev/null +++ b/pkgbuilds/cua-driver-bin/PKGBUILD @@ -0,0 +1,113 @@ +# Maintainer: Spencer Bull + +# cua-driver ships prebuilt from the trycua/cua monorepo release feed, so this +# repackages the vendor tarball. Upstream keeps the CLI, its cursor-theme +# compiler, and the SDK artifacts together in one directory and exposes the +# CLI through a symlink -- the binary resolves its helpers as siblings of +# /proc/self/exe -- so the whole tree lands under /usr/lib/cua-driver with a +# /usr/bin symlink. .omarchy/upstream.sh rewrites the version and checksums +# below from the release feed. +# +# The binary carries its own updater: `cua-driver update --apply` pipes the +# vendor installer into bash, which would install a second copy under +# ~/.cua-driver and link it into ~/.local/bin, stepping around pacman and +# this repository's release gate. Upstream offers no switch for that path, +# and a /usr/bin wrapper would not cover it either, since the MCP +# configurations the binary generates record the resolved executable. So +# prepare() rewrites the installer URL inside the binary to point at pm.sh, +# a stand-in that declines and names pacman instead. + +pkgname=cua-driver-bin +pkgver=0.23.2 +pkgrel=1 +pkgdesc="Computer-use driver for native GUI apps: accessibility-tree snapshots and input injection" +arch=('x86_64' 'aarch64') +url="https://github.com/trycua/cua" +license=('MIT') +# at-spi2-core carries the AT-SPI accessibility bus the driver reads GUI +# trees through; the X libraries are linked, not dlopen'd. +depends=( + 'at-spi2-core' + 'gcc-libs' + 'glibc' + 'libx11' + 'libxcb' + 'libxext' + 'libxi' + 'libxkbcommon' +) +provides=("cua-driver=${pkgver}") +conflicts=('cua-driver') +# Prebuilt Rust binaries ship byte-exact apart from the installer rewrite +# below: their build ids are what upstream symbolication matches. +options=('!strip' '!debug') +source=('LICENSE' 'pm.sh') +source_x86_64=("https://github.com/trycua/cua/releases/download/cua-driver-rs-v${pkgver}/cua-driver-rs-${pkgver}-linux-x86_64.tar.gz") +source_aarch64=("https://github.com/trycua/cua/releases/download/cua-driver-rs-v${pkgver}/cua-driver-rs-${pkgver}-linux-arm64.tar.gz") +sha256sums=('c0779290c1d4783169aa3dbfb55feb505e563ef8a004bbf55298ceffcfbda8d9' + 'c76e251c3ed424200eac52bec35ba534336307fabd83a175ab0b47e2084ab0d8') +sha256sums_x86_64=('478e010d2b0426de9d8a07eb839802daa92137b33cb8affb93b991e91d76ce7e') +sha256sums_aarch64=('3ad2d6c7ca7356a08e534baa2f4ca84ad02cebb2d27caef66cb268b6df71210d') + +case "${CARCH}" in + x86_64) _platform="linux-x86_64" ;; + aarch64) _platform="linux-arm64" ;; +esac +_vendor_tree="cua-driver-rs-${pkgver}-${_platform}" + +# Rust strings carry their length out of band, so the replacement has to be +# exactly as long as the original: 32 bytes, which is what fixes the +# stand-in's short name and location. +_vendor_installer='https://cua.ai/driver/install.sh' +_pacman_installer='file:///usr/lib/cua-driver/pm.sh' + +prepare() { + cd "${srcdir}/${_vendor_tree}" + + if (( ${#_vendor_installer} != ${#_pacman_installer} )); then + echo "installer URLs must be the same length to rewrite in place" >&2 + return 1 + fi + + # The URL appears twice: once in the updater and once in the printed + # reinstall one-liner. Any other count means upstream moved the updater + # and this rewrite needs another look, so the build stops rather than + # shipping a live self-updater. + local found + found=$(grep -obUaF "${_vendor_installer}" cua-driver | wc -l) + if (( found != 2 )); then + echo "expected the vendor installer URL twice in cua-driver, found ${found}" >&2 + return 1 + fi + + local size_before size_after + size_before=$(stat -c %s cua-driver) + sed -i "s|${_vendor_installer//./\\.}|${_pacman_installer}|g" cua-driver + size_after=$(stat -c %s cua-driver) + + if (( size_before != size_after )) \ + || grep -qUaF "${_vendor_installer}" cua-driver \ + || (( $(grep -obUaF "${_pacman_installer}" cua-driver | wc -l) != 2 )); then + echo "installer URL rewrite did not land cleanly in cua-driver" >&2 + return 1 + fi +} + +package() { + cd "${srcdir}/${_vendor_tree}" + + # The vendor tree stays together: cua-driver execs cua-cursor-theme as a + # sibling of the resolved binary, and the SDK library, node runtime, ABI + # header, and the GNOME wayland-helper extension are versioned with it. + install -d "${pkgdir}/usr/lib/cua-driver" + cp -a . "${pkgdir}/usr/lib/cua-driver/" + chmod -R a+rX "${pkgdir}/usr/lib/cua-driver" + + # The path prepare() wrote into the binary. + install -Dm755 "${srcdir}/pm.sh" "${pkgdir}/usr/lib/cua-driver/pm.sh" + + install -d "${pkgdir}/usr/bin" + ln -s ../lib/cua-driver/cua-driver "${pkgdir}/usr/bin/cua-driver" + + install -Dm644 "${srcdir}/LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" +} diff --git a/pkgbuilds/cua-driver-bin/pm.sh b/pkgbuilds/cua-driver-bin/pm.sh new file mode 100644 index 0000000..e60fd6c --- /dev/null +++ b/pkgbuilds/cua-driver-bin/pm.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Stands in for the vendor installer. cua-driver-bin points the binary's +# `update --apply` here instead of https://cua.ai/driver/install.sh, which +# would otherwise install a second copy under ~/.cua-driver and link it into +# ~/.local/bin, stepping around pacman and the repository's release gate. The +# exit status is what `cua-driver update --apply` reports. +{ + echo "cua-driver is installed by pacman (cua-driver-bin), so the vendor installer is disabled." + echo "Upgrade it with pacman instead:" + echo " sudo pacman -Syu cua-driver-bin" +} >&2 +exit 1