diff --git a/pkgbuilds/claude-desktop/.omarchy/anthropic-release-signing.key b/pkgbuilds/claude-desktop/.omarchy/anthropic-release-signing.key new file mode 100644 index 00000000..69a4aa52 --- /dev/null +++ b/pkgbuilds/claude-desktop/.omarchy/anthropic-release-signing.key @@ -0,0 +1,29 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGnK73ABEACnbytJXkjweYrwIr0aLEFRlH+C0nF44KxFc7gQmJ6PjSPMGZAD +dxZcaixU7zZl8WxEpVO0wLmIH8cf2zGOdyuZg1Yaugk1vHb2b8WBhAGCQJdPgB8W +XquedepEYtk56uP/gCoTjJDUZluEGBHnlnuujSJ4orxEdhSykEoAUfJZGEILPpMd +bphFt/Sn+Eb/TxM5jpKPdwnv8AShNF/1mZU1fWTQq9tRKJUakZj04gdaDFElQXak +CtTij+GT6yoYCARSHwGO+PC/Pr6q4tc+D7LRjxSBvUWDoFSmlqb/PJ1hj9D/7I2O +e4XXniAPWMR56KvxHlzOzrNQdJujbJdSkCwh1ZijkSd3y8ayW5WYUTGdRab99NUw +agzlabe/VVF6kzJ0Scn5q3PihB2Y9Bwo0CKnkYk7a7KT77EWv0Kkq+VHmOtqX3a2 +hhX+b6a6ve9rzJ1qZYGj+obv/C3Sx1LzUjAfqVy7RJDf2uAoP5t2g8u/TkSpUxhM +VEjZBkSxYZhMyzQM6t8IgkUfnSrIPTHixbDWARZ4beMOBjxyPZK1nP7OOrNR3TkK +JtwLMQAabURCDnL0PjS0iwBTU4jtumBD1XSULyWuoTvMljrpQr1nV1oDyOt0OLqa +KA2McWtd9PdXhC8y2EIg7TmrTlJLfHYbdmkiCYj4J49Q8HWkN/6WE+RTUwARAQAB +tD5BbnRocm9waWMgQ2xhdWRlIENvZGUgUmVsZWFzZSBTaWduaW5nIDxzZWN1cml0 +eUBhbnRocm9waWMuY29tPokCUQQTAQoAOxYhBDHd3iTd+rZ59C170rqpKf8afsrO +BQJpyu9wAhsPBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJELqpKf8afsrO +l5IP/2I8X1dFy5xYczWB/coIxGjuzS/V6ByZGZZEJsbr04pmuHiFUykJqPGWGQ6q +U0YF5iEwvEkaagS5m7DzhSEf3FM3Cgafax/6d70tar9Vr1D+w6uPfxetu7u/WYJp +aolIsdh5fTrBh9zSM1Njl8FM8wG8CwZQjS33Oa7d8cwRkgdUWbt6LXgz+cTQNuBn +BgW6Ks7oZFI25dfu0ojDR+aDFJg4+4wZoyDLPvJz1SIrJ5WFGs67zsx9SfS3yZnf +XKmBe+f0dUy+GJ2nFZrXFf99+c0dPEHYO8DCeAHZizjkFrdYtUHdDU0YDYEGkLJa +bE+pgcpkHf5EvsZzHsyDbl95W/eh7pcXMbwkN+W4CBYUE9X4uHhqzWaC5yAVRWUA +1BJ9V4LjZfHPLEJt0I3TxzXiEg9/BVeaTYq9RjaxIFo9Nfk158HqJY6SA5jslBlx +Gv/No8u+xVcze2UJyGVfEIUfm92+0UAIkny3+5cuVV0ICzJxXlXj0CnLM9Lt50wE +p3suVwuBEviCbZ08eAH1Ht8gbBdSsiOkIU8CX3v/scwHHx5q0+NBL6xLrQObg13a +tRXBlKObfElkPN3lTUbUnJOW4U8uSjH8VRP+AujKWMDFe7x0zCs+iYY1mTOvbrTS +9n3CmZUmbynZ+E/QWNENpW/pDNZdWFy43PASmML5FHu4m9Sn +=oqMI +-----END PGP PUBLIC KEY BLOCK----- diff --git a/pkgbuilds/claude-desktop/.omarchy/package.json b/pkgbuilds/claude-desktop/.omarchy/package.json new file mode 100644 index 00000000..bbe9ae19 --- /dev/null +++ b/pkgbuilds/claude-desktop/.omarchy/package.json @@ -0,0 +1,5 @@ +{ + "source": "local", + "release_ring": "fast", + "min_release_age": "24h" +} diff --git a/pkgbuilds/claude-desktop/.omarchy/upstream.sh b/pkgbuilds/claude-desktop/.omarchy/upstream.sh new file mode 100755 index 00000000..e13b740e --- /dev/null +++ b/pkgbuilds/claude-desktop/.omarchy/upstream.sh @@ -0,0 +1,190 @@ +#!/bin/bash +# Anthropic ships the Claude desktop app from its own Debian repository, and +# signs that repository's index. Reading the index costs three small HTTP +# requests instead of two half-gigabyte downloads, the pool keeps old versions +# so the URLs pinned in the PKGBUILD stay resolvable after the next release, +# and -- the point of doing it this way -- every checksum that reaches the +# PKGBUILD has been carried there under Anthropic's own signature: +# +# the bundled key, pinned by fingerprint -> signs InRelease +# InRelease -> hashes Packages +# Packages -> hashes each .deb +# +# A break anywhere in that chain fails the sync rather than proposing a +# checksum nobody vouched for. +set -euo pipefail + +BASE_URL="https://downloads.claude.ai/claude-desktop/apt/stable" +declare -A DEB_ARCHES=([x86_64]=amd64 [aarch64]=arm64) + +# "Anthropic Claude Code Release Signing ", the +# trust anchor for everything below. Cross-checked against four independent +# sources: the published install docs, downloads.claude.ai/claude-desktop/ +# key.asc, the InRelease signature itself, and the copy embedded in the +# .deb's own postinst. The key is committed next to this hook rather than +# fetched, so rotating it is a reviewed change to this package, not +# something the server can do to us. +KEY_FILE=".omarchy/anthropic-release-signing.key" +KEY_FPR='31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE' + +MIN_AGE="${MIN_RELEASE_AGE_SECONDS:-0}" +BYPASS="${BYPASS_MIN_RELEASE_AGE:-}" + +WORK_DIR=$(mktemp -d) +trap 'rm -rf "$WORK_DIR"' EXIT + +fail() { + echo "$*" >&2 + exit 1 +} + +# --- trust anchor ----------------------------------------------------------- +GNUPGHOME="$WORK_DIR/gnupg" +export GNUPGHOME +install -dm700 "$GNUPGHOME" + +[[ -f "$KEY_FILE" ]] || fail "Signing key missing: $KEY_FILE" +gpg --batch --quiet --import "$KEY_FILE" 2>/dev/null || + fail "Could not import $KEY_FILE" + +got_fpr=$(gpg --batch --with-colons --fingerprint | awk -F: '/^fpr:/{print $10; exit}') +[[ "$got_fpr" == "$KEY_FPR" ]] || + fail "Bundled key is not Anthropic's: got ${got_fpr:-none}, expected $KEY_FPR" + +# --- the signed index ------------------------------------------------------- +curl -fsSL -o "$WORK_DIR/InRelease" "$BASE_URL/dists/stable/InRelease" || + fail "Could not fetch InRelease" + +# Written out and read back rather than verified in place: gpg reports a good +# signature on a clearsigned file that carries unsigned text outside the armour. +gpg --batch --yes --output "$WORK_DIR/Release" --decrypt "$WORK_DIR/InRelease" >/dev/null 2>&1 || + fail "InRelease is not signed by Anthropic's release key" + +# Each architecture's Packages index, authenticated by the hash InRelease +# signs for it. Scoped to the SHA256 block: the same filenames recur under +# SHA512, and taking the first match regardless would compare a SHA256 sum +# against a SHA512 entry. +fetch_packages() { + local deb_arch="$1" path="main/binary-${1}/Packages" + local out="$WORK_DIR/Packages.$deb_arch" want got + + want=$(awk -v path="$path" ' + /^SHA256:/ { in_block = 1; next } + /^[A-Za-z][A-Za-z0-9-]*:/ { in_block = 0 } + in_block && $3 == path && length($1) == 64 { print $1; exit } + ' "$WORK_DIR/Release") + [[ -n "$want" ]] || fail "InRelease carries no SHA256 for $path" + + curl -fsSL -o "$out" "$BASE_URL/dists/stable/$path" || + fail "Could not fetch $path" + + got=$(sha256sum "$out" | cut -d' ' -f1) + [[ "$got" == "$want" ]] || + fail "$path does not match the hash InRelease signs for it" + + echo "$out" +} + +# " " per stanza, for our package only. The pool is shared, +# and a stanza that carries no checksum must not donate its version to the +# next one, so both fields reset at each Package: head. +releases_in() { + awk ' + { sub(/\r$/, "") } + /^Package:/ { pkg = $2; version = sha256 = "" } + /^Version:/ { version = $2 } + /^SHA256:/ { sha256 = $2 } + /^$/ { if (pkg == "claude-desktop" && version && sha256) print version, sha256 + pkg = version = sha256 = "" } + END { if (pkg == "claude-desktop" && version && sha256) print version, sha256 } + ' "$1" +} + +declare -A CHECKSUMS=() # " " -> sha256 +declare -A SEEN_COUNT=() # version -> number of arches offering it + +for arch in "${!DEB_ARCHES[@]}"; do + deb_arch="${DEB_ARCHES[$arch]}" + packages=$(fetch_packages "$deb_arch") + + while read -r version sha256; do + [[ -n "$version" ]] || continue + CHECKSUMS["$arch $version"]="$sha256" + SEEN_COUNT[$version]=$(( ${SEEN_COUNT[$version]:-0} + 1 )) + done < <(releases_in "$packages") +done + +# A release lands one architecture at a time and a single pkgver covers both, +# so only versions present in every architecture are candidates. +candidates=() +for version in "${!SEEN_COUNT[@]}"; do + (( SEEN_COUNT[$version] == ${#DEB_ARCHES[@]} )) && candidates+=("$version") +done +(( ${#candidates[@]} )) || fail "No release found for every architecture in the signed index" + +# Newest first, by pacman's comparator -- the one that decides whether a +# published package is an upgrade. sort -V disagrees with it at the corners. +newest_first=() +while (( ${#candidates[@]} )); do + best_index=0 + for i in "${!candidates[@]}"; do + if [[ $(vercmp "${candidates[$i]}" "${candidates[$best_index]}") -gt 0 ]]; then + best_index=$i + fi + done + newest_first+=("${candidates[$best_index]}") + unset 'candidates[best_index]' + candidates=("${candidates[@]}") +done + +# The pool serves a Last-Modified for each .deb, which is when that build was +# actually published. A release counts as published when its last architecture +# lands, so the newer of the two timestamps is the conservative one to age +# against. +published_at_of() { + local version="$1" newest_epoch=0 arch deb_arch header lm epoch + + for arch in "${!DEB_ARCHES[@]}"; do + deb_arch="${DEB_ARCHES[$arch]}" + header=$(curl -fsSLI \ + "$BASE_URL/pool/main/c/claude-desktop/claude-desktop_${version}_${deb_arch}.deb" 2>/dev/null) || return 1 + lm=$(awk -F': ' 'tolower($1) == "last-modified" { sub(/\r$/, "", $2); print $2; exit }' <<<"$header") + [[ -n "$lm" ]] || return 1 + epoch=$(date --date="$lm" +%s 2>/dev/null) || return 1 + (( epoch > newest_epoch )) && newest_epoch=$epoch + done + + (( newest_epoch > 0 )) || return 1 + date -u --date="@$newest_epoch" +%Y-%m-%dT%H:%M:%SZ +} + +# Walk newest to oldest and report the first release that has cleared the +# quarantine window, so a bad release held back does not also hold back the +# good one before it. +now=$(date +%s) +for version in "${newest_first[@]}"; do + published_at=$(published_at_of "$version") || + fail "Could not establish a publication time for $version" + + if (( MIN_AGE > 0 )) && [[ "$BYPASS" != "1" ]]; then + published_epoch=$(date --date="$published_at" +%s) + if (( now - published_epoch < MIN_AGE )); then + continue + fi + fi + + jq -n \ + --arg pkgver "$version" \ + --arg published_at "$published_at" \ + --arg x86_64 "${CHECKSUMS["x86_64 $version"]}" \ + --arg aarch64 "${CHECKSUMS["aarch64 $version"]}" \ + '{ + pkgver: $pkgver, + published_at: $published_at, + sha256sums: { x86_64: [$x86_64], aarch64: [$aarch64] } + }' + exit 0 +done + +# Everything on offer is still inside the quarantine window. +echo '{}' diff --git a/pkgbuilds/claude-desktop/PKGBUILD b/pkgbuilds/claude-desktop/PKGBUILD new file mode 100644 index 00000000..fcf94637 --- /dev/null +++ b/pkgbuilds/claude-desktop/PKGBUILD @@ -0,0 +1,160 @@ +# Maintainer: Arseniy Zarechnev +# +# The dependency set and the two Cowork path shims below were worked out by +# the AUR claude-desktop package (Kevin Diaz) and ya-claude (Aaron Bockelie); +# both were read closely while writing this. The signature-verifying upstream +# hook is new here. +# +# Anthropic publishes the desktop app only as a .deb, from their own apt +# repository; the docs point non-Debian distributions at the CLI instead. +# This package unpacks that .deb, which is the whole of it -- nothing is +# patched, added, or rebuilt. +# +# Like openai-codex-desktop, Omarchy tracks the vendor's own repository +# rather than the AUR: releases land several times a week. +# .omarchy/upstream.sh rewrites the version and checksums below from +# Anthropic's package index, after verifying that index's PGP signature. + +pkgname=claude-desktop +pkgver=1.40609.1 +pkgrel=1 +pkgdesc="Official Claude desktop app: Chat, Cowork, and Claude Code" +arch=('x86_64' 'aarch64') +url="https://claude.com/download" +license=('LicenseRef-Proprietary') + +# Two sources, reconciled: upstream's own "Depends:" field translated to Arch +# names (libgtk-3-0 -> gtk3, and so on), plus the libraries the shipped +# binaries actually link. namcap calls some of these unneeded -- those are +# reached through dlopen(), a spawned program, or D-Bus, none of which it can +# see in an ELF header. alsa-lib is only a "Recommends:" upstream, but +# Chromium needs it for audio. +depends=( + 'alsa-lib' + 'at-spi2-core' + 'cairo' + 'dbus' + 'expat' + 'gcc-libs' + 'glib2' + 'glibc' + 'gtk3' + 'hicolor-icon-theme' + 'libcap-ng' + 'libcups' + 'libdrm' + 'libnotify' + 'libseccomp' + 'libsecret' + 'libx11' + 'libxcb' + 'libxcomposite' + 'libxdamage' + 'libxext' + 'libxfixes' + 'libxkbcommon' + 'libxrandr' + 'libxtst' + 'mesa' + 'nspr' + 'nss' + 'pango' + 'socat' + 'systemd-libs' + 'util-linux-libs' + 'virtiofsd' + 'xdg-desktop-portal' + 'xdg-utils' +) + +# Cowork runs its agent inside a QEMU VM. Upstream ships the VM stack under +# "Recommends:", which apt installs by default, so depending on it here gives +# the same working-out-of-the-box Cowork that Debian and Ubuntu users get. +depends_x86_64=('qemu-system-x86' 'edk2-ovmf') +depends_aarch64=('qemu-system-aarch64' 'edk2-aarch64') + +optdepends=( + 'gnome-keyring: credential storage via Secret Service' + 'kwallet: credential storage on KDE Plasma' + 'libayatana-appindicator: system tray icon' + 'xdg-desktop-portal-hyprland: portal backend on Hyprland' +) + +makedepends=('libarchive') +# Other packagings of the same app, all of which own the same paths. +conflicts=('claude' 'claude-desktop-appimage' 'claude-desktop-bin' + 'claude-desktop-extra' 'ya-claude') + +# Prebuilt Electron: stripping corrupts the V8 snapshot and the embedded +# resources, and there is no source here to build a -debug package from. +options=('!strip' '!debug') + +# Omarchy: same reasoning as openai-codex-desktop -- maximum zstd is worth the +# extra build minutes on a half-gigabyte Electron tree that every user +# re-downloads on each of Anthropic's frequent releases. +COMPRESSZST=(zstd -c -z -q --ultra -22 --threads=0 -) + +_deb_x86_64="${pkgname}_${pkgver}_amd64.deb" +_deb_aarch64="${pkgname}_${pkgver}_arm64.deb" +_pool="https://downloads.claude.ai/claude-desktop/apt/stable/pool/main/c/claude-desktop" +source=('claude-desktop-launcher.sh') +source_x86_64=("${_deb_x86_64}::${_pool}/${_deb_x86_64}") +source_aarch64=("${_deb_aarch64}::${_pool}/${_deb_aarch64}") +noextract=("${_deb_x86_64}" "${_deb_aarch64}") +sha256sums=('ce6de7bdfa99f6be6442ab2c16cf8890e8dce520d60e02d65ceaeb0c0c4dddfc') +sha256sums_x86_64=('80182e8511c6bbee6de26c7ee225fbd2a9aba2274ef1405a1d89cd8fe7a380dc') +sha256sums_aarch64=('493ccc06030c5dbb225add135823385d8cfb400a5922704de377ea8f55b39c40') + +package() { + cd "${srcdir}" + + local deb_var="_deb_${CARCH}" + local deb="${!deb_var}" + + # Only the payload. The .deb's maintainer scripts register Anthropic's apt + # repository, install an AppArmor profile gated on Ubuntu's userns + # restriction, and register a GNOME Shell search provider -- none of which + # applies here, so none of it is reproduced. + bsdtar -xOf "${deb}" data.tar.xz | + bsdtar --no-same-owner -xf - -C "${pkgdir}" + + # Chromium's setuid sandbox helper, for kernels without unprivileged user + # namespaces. The payload already carries this mode; setting it explicitly + # records that the setuid bit is deliberate. + chmod 4755 "${pkgdir}/usr/lib/claude-desktop/chrome-sandbox" + + # /usr/bin/claude-desktop ships as a symlink to the Electron binary. Replace + # it with a launcher that names the safeStorage backend when a Secret + # Service is actually present -- without it, Electron's own detection falls + # through to plaintext on every non-GNOME, non-KDE session (so, on Omarchy), + # and the app refuses to persist a sign-in it cannot encrypt. + rm "${pkgdir}/usr/bin/claude-desktop" + install -Dm755 claude-desktop-launcher.sh "${pkgdir}/usr/bin/claude-desktop" + + install -Dm644 "${pkgdir}/usr/share/doc/claude-desktop/copyright" \ + "${pkgdir}/usr/share/licenses/${pkgname}/copyright" + + # --- Cowork path shims ---------------------------------------------------- + # The app looks for the VM stack where Debian puts it. + # + # virtiofsd: resolved from /usr/libexec then /usr/bin, with the bundled copy + # used only on Ubuntu 22.x, so on Arch the system binary is the one reached. + # Arch installs it in /usr/lib. + ln -s ../lib/virtiofsd "${pkgdir}/usr/bin/virtiofsd" + + # UEFI firmware: on x86_64 the app opens /usr/share/OVMF/OVMF_CODE_4M.fd and + # derives the VARS path from it by substring replacement, so both links are + # needed. /usr/share/OVMF is Arch's compat symlink to /usr/share/edk2, where + # the firmware lives under x64/ with different names. + # + # aarch64 needs no shim: there the app opens /usr/share/AAVMF/AAVMF_CODE.fd, + # and edk2-aarch64 already installs it at exactly that path. + if [[ $CARCH == x86_64 ]]; then + install -d "${pkgdir}/usr/share/edk2" + ln -s x64/OVMF_CODE.4m.fd "${pkgdir}/usr/share/edk2/OVMF_CODE_4M.fd" + ln -s x64/OVMF_VARS.4m.fd "${pkgdir}/usr/share/edk2/OVMF_VARS_4M.fd" + fi + + # Debian package-policy files are not used on Arch Linux. + rm -rf "${pkgdir}/usr/share/doc" "${pkgdir}/usr/share/lintian" +} diff --git a/pkgbuilds/claude-desktop/claude-desktop-launcher.sh b/pkgbuilds/claude-desktop/claude-desktop-launcher.sh new file mode 100755 index 00000000..163505bd --- /dev/null +++ b/pkgbuilds/claude-desktop/claude-desktop-launcher.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Chromium cannot reliably infer the Secret Service password-store backend +# from a Hyprland session, even when GNOME Keyring is already providing it -- +# the same problem hermes-desktop hit, fixed the same way (246eea9). Left to +# its own detection the app intermittently decides no keyring is available, +# declines to persist the sign-in, and says so in a toast. +# +# Two cases are deliberately left alone: an explicit --password-store from the +# caller, and KDE, where the app already appends --password-store=kwalletd6 +# itself and carries fallback logic for a KWallet with no wallet. +# CLAUDE_DESKTOP_PASSWORD_STORE overrides the choice; "none" hands the +# decision back to Electron. +set -uo pipefail + +APP="/usr/lib/claude-desktop/claude-desktop" + +# An explicit flag, in either --flag=value or --flag value form. +for arg in "$@"; do + case "$arg" in + --password-store | --password-store=*) exec "$APP" "$@" ;; + esac +done + +case "${XDG_CURRENT_DESKTOP:-}" in + *KDE* | *Plasma* | *plasma*) exec "$APP" "$@" ;; +esac + +store="${CLAUDE_DESKTOP_PASSWORD_STORE:-gnome-libsecret}" +[[ "$store" == "none" ]] && exec "$APP" "$@" + +exec "$APP" --password-store="$store" "$@"