]> vilimpoc.org git repositories - dotfiles/commitdiff
dotfiles: install AGI from setup-linux.sh, on Ubuntu and on CachyOS master
authorMax Vilimpoc <max@vilimpoc.org>
Mon, 31 Aug 2026 09:20:55 +0000 (11:20 +0200)
committerMax Vilimpoc <max@vilimpoc.org>
Mon, 31 Aug 2026 09:20:55 +0000 (11:20 +0200)
The script died on anything without apt-get, which is most of the machines it
now has to run on, so the package layer is a two-line wrapper over apt-get and
pacman and the rsync lists gained their Arch spellings -- base-devel, pkgconf,
and the libraries without the -dev half Arch does not split out.

AGI ships a .deb and a .zip per release; Debian and Ubuntu get the .deb, Arch
and CachyOS the .zip unpacked to /opt/agi, since there is no AUR package for it
(the AUR RPC knows no agi, agi-bin or android-graphics-inspector).  The .zip
route replays the .desktop and MIME files the .deb installs, so both routes
leave the same system behind, and only ever replaces a /opt/agi that carries a
build.properties.

The .deb cannot be installed as it stands.  It declares "Depends:
openjdk-22-jre, libgtk-3-0, libwebkit2gtk-4.1-0" and Ubuntu has never packaged
openjdk-22 -- jammy through resolute ship 11, 17, 21 and 25 -- so apt has
nothing to resolve that against.  Nothing in the package needs 22 either: it
bundles no JRE, the Go launcher settles for >= 11, and the highest class file
version in gapic.jar is 63, i.e. Java 19.  openjdk-21-jre satisfies both and
exists on every release from 22.04 on, so the install picks a JRE that is
actually in the archive and rewrites that one control field to name it.

Only the control member is rebuilt, with ar; dpkg-deb -R/-b would unpack and
recompress 230MB of payload for a one-line edit.  Verified against the real
84MB package: member order preserved, Depends retargeted, and dpkg-deb still
parses the result.  Should any of that fail, the zip installer is the fallback,
so the .deb path can only cost time.

libgtk-3-0 became libgtk-3-0t64 in 24.04 and SWT wants webkit 4.1 rather than
the 4.0 only jammy has, so both are chosen by asking the archive rather than by
name.  adb is installed too: it is not a hard dependency, but gapis carries the
string "adb could not be found from ANDROID_HOME or PATH", which is all AGI can
do without one.

--agi-only and --no-agi split the two halves; --minimal implies --no-agi.
AGI_VERSION pins a release, otherwise the version comes from the releases API
with the last-checked 3.3.3 as the offline fallback, and a re-run whose
/opt/agi/build.properties already matches skips the 85MB download.  No pacman
-Sy anywhere -- refreshing the database without upgrading is how an Arch box
ends up half-broken -- so a stale database is reported rather than worked
around.

Exercised on CachyOS: every --dry-run path, the version lookup, and AGI 3.3.3
itself starting against the installed JRE with gtk3 and webkit2gtk-4.1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PqwcPKuLX7xML2DZqHnNJY

setup-linux.sh [changed mode: 0644->0755]

old mode 100644 (file)
new mode 100755 (executable)
index afb36a7..823dfa0
@@ -1,29 +1,41 @@
 #!/bin/bash
 #
-# setup-linux.sh -- install everything needed to build rsync on Ubuntu.
+# setup-linux.sh -- install the rsync build toolchain and AGI.
 #
-# Tested on Ubuntu 22.04.5 LTS and 26.04 LTS.  Covers both build systems:
-# the autoconf one (./configure && make) and the CMake one (CMakeLists.txt),
-# plus the optional feature libraries and the manpage generator.
+# Tested on Ubuntu 22.04.5 LTS and 26.04 LTS, and on CachyOS.  Covers both
+# rsync build systems: the autoconf one (./configure && make) and the CMake
+# one (CMakeLists.txt), plus the optional feature libraries and the manpage
+# generator.  Also installs AGI (Android GPU Inspector) from the latest
+# google/agi release: the .deb on Debian/Ubuntu, the .zip on Arch/CachyOS.
 #
 # Usage:
-#     ./setup-linux.sh              # install required + optional
-#     ./setup-linux.sh --minimal    # required only (no ACLs/xattrs/zstd/...)
+#     ./setup-linux.sh              # rsync required + optional, then AGI
+#     ./setup-linux.sh --minimal    # rsync required only, no AGI
+#     ./setup-linux.sh --no-agi     # rsync deps only
+#     ./setup-linux.sh --agi-only   # AGI and its dependencies only
 #     ./setup-linux.sh --dry-run    # show what would be installed
 #
-# Safe to re-run: apt skips anything already present.
+# AGI_VERSION=3.3.3 ./setup-linux.sh pins a release instead of asking GitHub
+# for the latest one.
+#
+# Safe to re-run: the package managers skip anything already present, and AGI
+# is re-downloaded only when the installed version is not the wanted one.
 
 set -uo pipefail
 
 MINIMAL=0
 DRY_RUN=0
+WANT_AGI=1
+WANT_RSYNC=1
 
 for arg in "$@"; do
     case "$arg" in
-    --minimal)  MINIMAL=1 ;;
+    --minimal)  MINIMAL=1; WANT_AGI=0 ;;
     --dry-run)  DRY_RUN=1 ;;
+    --no-agi)   WANT_AGI=0 ;;
+    --agi-only) WANT_RSYNC=0; WANT_AGI=1 ;;
     -h|--help)
-        sed -n '2,17p' "$0" | sed 's/^# \{0,1\}//'
+        sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//'
         exit 0 ;;
     *)
         echo "setup-linux.sh: unknown option '$arg' (try --help)" >&2
@@ -39,10 +51,52 @@ bold()  { printf '\033[1m%s\033[0m\n' "$*"; }
 
 die() { red "ERROR: $*"; exit 1; }
 
-# ------------------------------------------------------- sanity checks
+# ------------------------------------------------- package manager wrapper
+
+# Two families are supported: apt (Debian/Ubuntu) and pacman (Arch/CachyOS).
+PM=""
+if command -v apt-get >/dev/null 2>&1; then
+    PM=apt
+elif command -v pacman >/dev/null 2>&1; then
+    PM=pacman
+else
+    die "found neither apt-get nor pacman -- unsupported distribution."
+fi
+
+pkg_available() {
+    case "$PM" in
+    apt)
+        local cand
+        cand=$(apt-cache policy "$1" 2>/dev/null | awk '/Candidate:/{print $2}')
+        [ -n "$cand" ] && [ "$cand" != "(none)" ] ;;
+    pacman)
+        pacman -Si "$1" >/dev/null 2>&1 ;;
+    esac
+}
 
-command -v apt-get >/dev/null 2>&1 \
-    || die "no apt-get found -- this script is for Debian/Ubuntu systems."
+# Echo the first name the distro actually ships.  Package names drift between
+# releases (libgtk-3-0 became libgtk-3-0t64 in Ubuntu 24.04, and the JDK
+# version in the archive moves every six months), so nothing is hard-coded.
+pick_pkg() {
+    local p
+    for p in "$@"; do
+        if pkg_available "$p"; then
+            echo "$p"
+            return 0
+        fi
+    done
+    return 1
+}
+
+pkg_install() {
+    [ $# -eq 0 ] && return 0
+    case "$PM" in
+    apt)    apt-get install -y "$@" ;;
+    pacman) pacman -S --needed --noconfirm "$@" ;;
+    esac
+}
+
+# ------------------------------------------------------- sanity checks
 
 DISTRO="unknown"
 RELEASE="unknown"
@@ -53,20 +107,31 @@ if [ -r /etc/os-release ]; then
     RELEASE="${VERSION_ID:-unknown}"
 fi
 
-bold "rsync build dependencies"
+bold "rsync build dependencies and AGI"
 echo "  distro ......... ${PRETTY_NAME:-$DISTRO $RELEASE}"
 echo "  architecture ... $(uname -m)"
+echo "  packages ....... $PM"
 
 case "$DISTRO" in
-ubuntu|debian|linuxmint|pop) ;;
+ubuntu|debian|linuxmint|pop|arch|cachyos|endeavouros|manjaro) ;;
 *)  echo
-    red "Warning: this script targets Ubuntu/Debian; '$DISTRO' is untested."
+    red "Warning: '$DISTRO' is untested; assuming it behaves like a $PM distro."
     echo "Continuing anyway -- package names may differ."
     ;;
 esac
 
-# apt needs root.  Re-exec under sudo rather than sprinkling it around, so
-# that a single password prompt covers the whole run.
+# AGI ships x86-64 binaries only.
+case "$(uname -m)" in
+x86_64|amd64) ;;
+*)  if [ "$WANT_AGI" -eq 1 ]; then
+        red "Warning: AGI publishes x86-64 Linux builds only; skipping it."
+        WANT_AGI=0
+    fi
+    ;;
+esac
+
+# The package managers need root.  Re-exec under sudo rather than sprinkling
+# it around, so that a single password prompt covers the whole run.
 if [ "$(id -u)" -ne 0 ] && [ "$DRY_RUN" -eq 0 ]; then
     command -v sudo >/dev/null 2>&1 \
         || die "not running as root and sudo is not installed."
@@ -74,72 +139,210 @@ if [ "$(id -u)" -ne 0 ] && [ "$DRY_RUN" -eq 0 ]; then
     echo "Re-running under sudo..."
     # Plain sudo, not "sudo -E": some sudoers configs refuse to preserve the
     # environment, and the exports below happen in the root instance anyway.
-    exec sudo "$0" "$@"
+    exec sudo AGI_VERSION="${AGI_VERSION:-}" "$0" "$@"
 fi
 
 export DEBIAN_FRONTEND=noninteractive
 export NEEDRESTART_MODE=a          # don't prompt about restarting services
 
-# ------------------------------------------------------------- packages
-
-# Needed to build rsync at all.
-REQUIRED=(
-    build-essential     # gcc, g++, make, libc headers
-    gawk                # rsync needs a modern awk for its code generators
-    autoconf            # ./configure is generated from configure.ac
-    automake
-    python3             # manpage + header generators, and the test suite
-    cmake               # the CMake build
-    ninja-build         # ...and its default generator
-    git                 # version stamping (git-version.h)
-    pkg-config
-)
-
-# Optional: each one switches on an rsync feature.  Missing ones only mean
-# a smaller feature set, never a failed build.
-OPTIONAL=(
-    acl libacl1-dev         # --acls  (helper tools also used by the tests)
-    attr libattr1-dev       # --xattrs (likewise)
-    libxxhash-dev           # xxhash checksums (becomes the default)
-    libzstd-dev             # zstd compression (becomes the default)
-    liblz4-dev              # lz4 compression
-    libssl-dev              # OpenSSL MD4/MD5
-    zlib1g-dev              # for cmake -DRSYNC_EXTERNAL_ZLIB=ON
-    libpopt-dev             # for ./configure --with-included-popt=no
-)
-
-# Development tooling: not needed to build rsync, but useful when working on
-# its shell scripts -- including this one, which shellcheck keeps honest.
-DEVTOOLS=(
-    shellcheck
-)
-OPTIONAL+=("${DEVTOOLS[@]}")
-
-# The manpages need one of two python3 markdown libraries; upstream prefers
-# cmarkgfm.  Pick whichever this release actually offers.
-MARKDOWN_PKG=""
-for p in python3-cmarkgfm python3-commonmark; do
-    cand=$(apt-cache policy "$p" 2>/dev/null | awk '/Candidate:/{print $2}')
-    if [ -n "$cand" ] && [ "$cand" != "(none)" ]; then
-        MARKDOWN_PKG="$p"
-        break
+# ------------------------------------------------------- rsync packages
+
+REQUIRED=()
+OPTIONAL=()
+
+if [ "$WANT_RSYNC" -eq 1 ]; then
+    case "$PM" in
+    apt)
+        # Needed to build rsync at all.
+        REQUIRED=(
+            build-essential     # gcc, g++, make, libc headers
+            gawk                # rsync needs a modern awk for its code generators
+            autoconf            # ./configure is generated from configure.ac
+            automake
+            python3             # manpage + header generators, and the test suite
+            cmake               # the CMake build
+            ninja-build         # ...and its default generator
+            git                 # version stamping (git-version.h)
+            pkg-config
+        )
+
+        # Optional: each one switches on an rsync feature.  Missing ones only
+        # mean a smaller feature set, never a failed build.
+        OPTIONAL=(
+            acl libacl1-dev         # --acls  (helper tools also used by the tests)
+            attr libattr1-dev       # --xattrs (likewise)
+            libxxhash-dev           # xxhash checksums (becomes the default)
+            libzstd-dev             # zstd compression (becomes the default)
+            liblz4-dev              # lz4 compression
+            libssl-dev              # OpenSSL MD4/MD5
+            zlib1g-dev              # for cmake -DRSYNC_EXTERNAL_ZLIB=ON
+            libpopt-dev             # for ./configure --with-included-popt=no
+        )
+        ;;
+    pacman)
+        # Arch splits nothing out into -dev packages: the headers ship with
+        # the library, and base-devel covers the compiler toolchain.
+        REQUIRED=(
+            base-devel          # gcc, make, and friends
+            gawk
+            autoconf
+            automake
+            python
+            cmake
+            ninja
+            git
+            pkgconf
+        )
+        OPTIONAL=(
+            acl                     # --acls
+            attr                    # --xattrs
+            xxhash                  # xxhash checksums (becomes the default)
+            zstd                    # zstd compression (becomes the default)
+            lz4                     # lz4 compression
+            openssl                 # OpenSSL MD4/MD5
+            zlib                    # for cmake -DRSYNC_EXTERNAL_ZLIB=ON
+            popt                    # for ./configure --with-included-popt=no
+        )
+        ;;
+    esac
+
+    # Development tooling: not needed to build rsync, but useful when working
+    # on its shell scripts -- including this one, which shellcheck keeps honest.
+    OPTIONAL+=(shellcheck)
+
+    # The manpages need one of two python3 markdown libraries; upstream prefers
+    # cmarkgfm.  Pick whichever this release actually offers.
+    case "$PM" in
+    apt)    MARKDOWN_PKG=$(pick_pkg python3-cmarkgfm python3-commonmark) ;;
+    pacman) MARKDOWN_PKG=$(pick_pkg python-cmarkgfm python-commonmark) ;;
+    esac
+    if [ -n "$MARKDOWN_PKG" ]; then
+        OPTIONAL+=("$MARKDOWN_PKG")
+    fi
+
+    if [ "$MINIMAL" -eq 1 ]; then
+        OPTIONAL=()
     fi
-done
-if [ -n "$MARKDOWN_PKG" ]; then
-    OPTIONAL+=("$MARKDOWN_PKG")
 fi
 
-if [ "$MINIMAL" -eq 1 ]; then
-    OPTIONAL=()
+# ------------------------------------------------------- AGI packages
+
+# AGI is a Java/SWT desktop app plus a set of Go binaries.  The .deb declares
+# "Depends: openjdk-22-jre, libgtk-3-0, libwebkit2gtk-4.1-0", but Ubuntu has
+# never packaged openjdk-22 -- so pick a JRE that exists instead.  The GUI's
+# class files are Java 19 bytecode, so anything from 21 up runs it; the launcher
+# itself only insists on 11.  adb is not a hard dependency, but without it AGI
+# cannot see Android devices ("adb could not be found from ANDROID_HOME or PATH").
+AGI_DEPS=()
+AGI_JRE=""
+AGI_GTK=""
+AGI_WEBKIT=""
+AGI_ADB=""
+
+if [ "$WANT_AGI" -eq 1 ]; then
+    case "$PM" in
+    apt)
+        AGI_JRE=$(pick_pkg openjdk-21-jre openjdk-25-jre openjdk-24-jre \
+                           openjdk-23-jre openjdk-22-jre openjdk-26-jre)
+        AGI_GTK=$(pick_pkg libgtk-3-0t64 libgtk-3-0)
+        AGI_WEBKIT=$(pick_pkg libwebkit2gtk-4.1-0)
+        AGI_ADB=$(pick_pkg adb android-sdk-platform-tools)
+        # curl fetches the release, binutils' ar rewrites the .deb's control
+        # member, ca-certificates lets curl trust github.com.
+        AGI_DEPS=(curl ca-certificates binutils xz-utils unzip)
+        ;;
+    pacman)
+        AGI_JRE=$(pick_pkg jre-openjdk jre21-openjdk)
+        AGI_GTK=$(pick_pkg gtk3)
+        AGI_WEBKIT=$(pick_pkg webkit2gtk-4.1)
+        AGI_ADB=$(pick_pkg android-tools)
+        AGI_DEPS=(curl unzip)
+        ;;
+    esac
+
+    for p in "$AGI_GTK" "$AGI_WEBKIT" "$AGI_ADB"; do
+        [ -n "$p" ] && AGI_DEPS+=("$p")
+    done
+
+    # A JRE already on the box is good enough if it is new enough to load the
+    # GUI's Java 19 class files.  On apt we install one anyway: the .deb's
+    # rewritten Depends line has to name a package dpkg knows about.
+    JAVA_MAJOR=0
+    if command -v java >/dev/null 2>&1; then
+        JAVA_MAJOR=$(java -version 2>&1 \
+            | sed -n '1s/.*version "\([0-9]\{1,\}\).*/\1/p')
+        JAVA_MAJOR=${JAVA_MAJOR:-0}
+    fi
+    if [ -n "$AGI_JRE" ] && { [ "$PM" = apt ] || [ "$JAVA_MAJOR" -lt 19 ]; }; then
+        AGI_DEPS+=("$AGI_JRE")
+    fi
+fi
+
+# ------------------------------------------------------- AGI release info
+
+# Used only when GitHub cannot be reached; the release that this script was
+# last checked against.
+AGI_FALLBACK_VERSION=3.3.3
+AGI_WANTED=""
+AGI_INSTALLED=""
+
+agi_installed_version() {
+    [ -r /opt/agi/build.properties ] || return 1
+    awk -F= '
+        /^Version\.Major/ { maj = $2 }
+        /^Version\.Minor/ { min = $2 }
+        /^Version\.Micro/ { mic = $2 }
+        END { if (maj == "") exit 1; print maj "." min "." mic }
+    ' /opt/agi/build.properties | tr -d ' \r'
+}
+
+agi_latest_version() {
+    command -v curl >/dev/null 2>&1 || return 1
+    curl -fsSL --retry 3 --max-time 20 \
+        https://api.github.com/repos/google/agi/releases/latest 2>/dev/null \
+        | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"v\{0,1\}\([^"]*\)".*/\1/p' \
+        | head -1
+}
+
+if [ "$WANT_AGI" -eq 1 ]; then
+    AGI_WANTED="${AGI_VERSION:-}"
+    if [ -z "$AGI_WANTED" ]; then
+        AGI_WANTED=$(agi_latest_version)
+    fi
+    if [ -z "$AGI_WANTED" ]; then
+        red "Could not reach the GitHub releases API; falling back to AGI $AGI_FALLBACK_VERSION."
+        AGI_WANTED="$AGI_FALLBACK_VERSION"
+    fi
+    AGI_INSTALLED=$(agi_installed_version) || AGI_INSTALLED=""
 fi
 
+AGI_BASE_URL="https://github.com/google/agi/releases/download"
+
+# ------------------------------------------------------------- dry run
+
 if [ "$DRY_RUN" -eq 1 ]; then
-    echo
-    bold "Would install (required):"
-    printf '  %s\n' "${REQUIRED[@]}"
-    if [ ${#OPTIONAL[@]} -gt 0 ]; then
-        bold "Would install (optional):"
-        printf '  %s\n' "${OPTIONAL[@]}"
+    if [ "$WANT_RSYNC" -eq 1 ]; then
+        echo
+        bold "Would install (rsync, required):"
+        printf '  %s\n' "${REQUIRED[@]}"
+        if [ ${#OPTIONAL[@]} -gt 0 ]; then
+            bold "Would install (rsync, optional):"
+            printf '  %s\n' "${OPTIONAL[@]}"
+        fi
+    fi
+    if [ "$WANT_AGI" -eq 1 ]; then
+        echo
+        bold "Would install (AGI dependencies):"
+        printf '  %s\n' "${AGI_DEPS[@]}"
+        bold "Would install AGI $AGI_WANTED:"
+        if [ "$PM" = apt ]; then
+            echo "  $AGI_BASE_URL/v$AGI_WANTED/agi-$AGI_WANTED-linux.deb"
+            echo "  via dpkg, with Depends retargeted at ${AGI_JRE:-the installed JRE}"
+        else
+            echo "  $AGI_BASE_URL/v$AGI_WANTED/agi-$AGI_WANTED-linux.zip"
+            echo "  unpacked into /opt/agi, symlinked as /usr/local/bin/agi"
+        fi
+        [ -n "$AGI_INSTALLED" ] && echo "  (currently installed: $AGI_INSTALLED)"
     fi
     exit 0
 fi
@@ -147,40 +350,213 @@ fi
 # ------------------------------------------------------------- install
 
 echo
-bold "Updating package lists..."
-if ! apt-get update -qq; then
-    red "apt-get update failed -- continuing with the cached lists."
-fi
+case "$PM" in
+apt)
+    bold "Updating package lists..."
+    if ! apt-get update -qq; then
+        red "apt-get update failed -- continuing with the cached lists."
+    fi
+    ;;
+pacman)
+    # No "pacman -Sy" here on purpose: refreshing the database without also
+    # upgrading is how Arch systems end up half-broken.  If a package fetch
+    # 404s below, the sync database is stale -- run "pacman -Syu" first.
+    bold "Using the existing pacman database (run 'pacman -Syu' if it is stale)."
+    ;;
+esac
 
-echo
-bold "Installing required packages..."
-if ! apt-get install -y "${REQUIRED[@]}"; then
-    # Fall back to one-at-a-time so the failing package is obvious.
-    red "Bulk install failed; retrying individually to find the culprit..."
-    FAILED=()
-    for p in "${REQUIRED[@]}"; do
-        apt-get install -y "$p" || FAILED+=("$p")
-    done
-    [ ${#FAILED[@]} -eq 0 ] || die "could not install: ${FAILED[*]}"
+OPT_FAILED=()
+
+if [ "$WANT_RSYNC" -eq 1 ]; then
+    echo
+    bold "Installing required packages..."
+    if ! pkg_install "${REQUIRED[@]}"; then
+        # Fall back to one-at-a-time so the failing package is obvious.
+        red "Bulk install failed; retrying individually to find the culprit..."
+        FAILED=()
+        for p in "${REQUIRED[@]}"; do
+            pkg_install "$p" || FAILED+=("$p")
+        done
+        [ ${#FAILED[@]} -eq 0 ] || die "could not install: ${FAILED[*]}"
+    fi
+
+    if [ ${#OPTIONAL[@]} -gt 0 ]; then
+        echo
+        bold "Installing optional packages..."
+        if ! pkg_install "${OPTIONAL[@]}"; then
+            red "Bulk install failed; retrying individually..."
+            for p in "${OPTIONAL[@]}"; do
+                pkg_install "$p" || OPT_FAILED+=("$p")
+            done
+        fi
+    fi
 fi
 
-OPT_FAILED=()
-if [ ${#OPTIONAL[@]} -gt 0 ]; then
+# ----------------------------------------------------------------- AGI
+
+# The .desktop and MIME files the .deb ships, replayed for the .zip install so
+# both routes leave the same system behind.
+agi_desktop_files() {
+    cat >/usr/share/applications/google-agi.desktop <<'DESKTOP'
+[Desktop Entry]
+Encoding=UTF-8
+Type=Application
+Name=Android GPU Inspector
+Comment=Android GPU Inspector
+Categories=Development;Debugger;Graphics;3DGraphics
+Icon=/opt/agi/icon.png
+Exec=/opt/agi/agi %f
+Terminal=false
+MimeType=application/x-gfxtrace;application/x-perfetto
+DESKTOP
+
+    mkdir -p /usr/share/mime/packages
+    cat >/usr/share/mime/packages/agi.xml <<'MIME'
+<?xml version="1.0" encoding="UTF-8"?>
+<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
+   <mime-type type="application/x-gfxtrace">
+     <comment>Graphics API Trace</comment>
+     <glob pattern="*.gfxtrace"/>
+   </mime-type>
+   <mime-type type="application/x-perfetto">
+     <comment>System Profile Trace</comment>
+     <glob pattern="*.perfetto"/>
+   </mime-type>
+</mime-info>
+MIME
+
+    command -v update-mime-database >/dev/null 2>&1 \
+        && update-mime-database /usr/share/mime >/dev/null 2>&1
+    command -v update-desktop-database >/dev/null 2>&1 \
+        && update-desktop-database /usr/share/applications >/dev/null 2>&1
+    return 0
+}
+
+agi_download() {
+    # $1 = url, $2 = destination file
+    echo "  fetching $(basename "$1")..."
+    curl -fL --retry 3 --progress-bar -o "$2" "$1"
+}
+
+# Every dependency the .deb names has to exist in the archive, or dpkg will
+# refuse the package.  openjdk-22-jre does not exist in any Ubuntu release,
+# so rewrite that line to the JRE we just installed.  Only the control member
+# is rebuilt -- unpacking and recompressing the 230MB payload with dpkg-deb
+# would take minutes and change nothing.
+deb_depends_satisfiable() {
+    local dep
+    while IFS= read -r dep; do
+        dep=$(echo "$dep" | sed 's/(.*)//; s/^[[:space:]]*//; s/[[:space:]]*$//')
+        [ -z "$dep" ] && continue
+        pkg_available "$dep" || return 1
+    done < <(dpkg-deb -f "$1" Depends 2>/dev/null | tr ',' '\n')
+    return 0
+}
+
+rewrite_deb_depends() {
+    # $1 = .deb, $2 = new Depends line, $3 = scratch directory
+    local deb="$1" depends="$2" work="$3/control"
+    command -v ar >/dev/null 2>&1 || return 1
+    rm -rf "$work"
+    mkdir -p "$work/c" || return 1
+    ( cd "$work" && ar x "$deb" control.tar.xz ) || return 1
+    tar xf "$work/control.tar.xz" -C "$work/c" || return 1
+    sed -i "s|^Depends:.*|Depends: $depends|" "$work/c/control" || return 1
+    rm -f "$work/control.tar.xz"
+    ( cd "$work/c" && tar cJf ../control.tar.xz . ) || return 1
+    ( cd "$work" && ar r "$deb" control.tar.xz ) || return 1
+    # Make sure dpkg still understands the archive we just edited.
+    dpkg-deb -f "$deb" Depends >/dev/null 2>&1 || return 1
+}
+
+install_agi_deb() {
+    local ver="$1" tmp="$2" deb="$tmp/agi.deb" depends
+
+    agi_download "$AGI_BASE_URL/v$ver/agi-$ver-linux.deb" "$deb" || return 1
+
+    if ! deb_depends_satisfiable "$deb"; then
+        depends="${AGI_JRE:-default-jre}"
+        [ -n "$AGI_GTK" ]    && depends="$depends, $AGI_GTK"
+        [ -n "$AGI_WEBKIT" ] && depends="$depends, $AGI_WEBKIT"
+        echo "  retargeting the package's Depends at: $depends"
+        rewrite_deb_depends "$deb" "$depends" "$tmp" || {
+            red "  could not rewrite the .deb's dependencies."
+            return 1
+        }
+    fi
+
+    if ! dpkg -i "$deb"; then
+        red "  dpkg reported a problem; asking apt to resolve it..."
+        apt-get -f install -y || return 1
+        dpkg -i "$deb" || return 1
+    fi
+}
+
+install_agi_zip() {
+    local ver="$1" tmp="$2"
+
+    agi_download "$AGI_BASE_URL/v$ver/agi-$ver-linux.zip" "$tmp/agi.zip" || return 1
+    unzip -q "$tmp/agi.zip" -d "$tmp/unpacked" || return 1
+    [ -x "$tmp/unpacked/agi/agi" ] || {
+        red "  the zip did not contain agi/agi -- leaving /opt/agi alone."
+        return 1
+    }
+
+    # Only ever replace a directory that looks like a previous AGI install.
+    if [ -e /opt/agi ] && [ ! -e /opt/agi/build.properties ]; then
+        red "  /opt/agi exists but is not an AGI install; refusing to replace it."
+        return 1
+    fi
+
+    rm -rf /opt/agi
+    mv "$tmp/unpacked/agi" /opt/agi || return 1
+    chown -R root:root /opt/agi
+    ln -sf /opt/agi/agi /usr/local/bin/agi
+    agi_desktop_files
+}
+
+AGI_OK=0
+if [ "$WANT_AGI" -eq 1 ]; then
     echo
-    bold "Installing optional packages..."
-    if ! apt-get install -y "${OPTIONAL[@]}"; then
+    bold "Installing AGI dependencies..."
+    if ! pkg_install "${AGI_DEPS[@]}"; then
         red "Bulk install failed; retrying individually..."
-        for p in "${OPTIONAL[@]}"; do
-            apt-get install -y "$p" || OPT_FAILED+=("$p")
+        for p in "${AGI_DEPS[@]}"; do
+            pkg_install "$p" || OPT_FAILED+=("$p")
         done
     fi
+
+    echo
+    if [ -n "$AGI_INSTALLED" ] && [ "$AGI_INSTALLED" = "$AGI_WANTED" ]; then
+        bold "AGI $AGI_INSTALLED is already installed in /opt/agi."
+        AGI_OK=1
+    else
+        bold "Installing AGI $AGI_WANTED..."
+        [ -n "$AGI_INSTALLED" ] && echo "  replacing the installed $AGI_INSTALLED"
+        AGI_TMP=$(mktemp -d "${TMPDIR:-/tmp}/agi-install.XXXXXX") \
+            || die "could not create a temporary directory."
+        # 85MB of .deb or .zip; do not leave it behind on any exit path.
+        trap 'rm -rf "$AGI_TMP"' EXIT
+
+        if [ "$PM" = apt ]; then
+            if install_agi_deb "$AGI_WANTED" "$AGI_TMP"; then
+                AGI_OK=1
+            else
+                red "  .deb install failed; falling back to the zip release."
+                install_agi_zip "$AGI_WANTED" "$AGI_TMP" && AGI_OK=1
+            fi
+        else
+            install_agi_zip "$AGI_WANTED" "$AGI_TMP" && AGI_OK=1
+        fi
+
+        rm -rf "$AGI_TMP"
+        trap - EXIT
+        [ "$AGI_OK" -eq 1 ] || red "AGI installation failed."
+    fi
 fi
 
 # --------------------------------------------------------------- verify
 
-echo
-bold "Verifying the toolchain..."
-
 MISSING=()
 check_cmd() {
     if command -v "$1" >/dev/null 2>&1; then
@@ -191,54 +567,99 @@ check_cmd() {
     fi
 }
 
-for c in gcc g++ make gawk autoconf automake cmake ninja python3 git; do
-    check_cmd "$c"
-done
+if [ "$WANT_RSYNC" -eq 1 ]; then
+    echo
+    bold "Verifying the toolchain..."
 
-# Not required to build, so report it without failing the run.
-if command -v shellcheck >/dev/null 2>&1; then
-    printf '  %-16s %s\n' "shellcheck" "$(command -v shellcheck)"
-else
-    printf '  %-16s %s\n' "shellcheck" "not installed (optional)"
+    for c in gcc g++ make gawk autoconf automake cmake ninja python3 git; do
+        check_cmd "$c"
+    done
+
+    # Not required to build, so report it without failing the run.
+    if command -v shellcheck >/dev/null 2>&1; then
+        printf '  %-16s %s\n' "shellcheck" "$(command -v shellcheck)"
+    else
+        printf '  %-16s %s\n' "shellcheck" "not installed (optional)"
+    fi
+
+    echo
+    bold "Verifying optional libraries..."
+    check_header() {
+        # $1 = human name, $2 = header path
+        if [ -e "/usr/include/$2" ] || \
+           find /usr/include -maxdepth 3 -name "$(basename "$2")" -print -quit \
+                2>/dev/null | grep -q .; then
+            printf '  %-16s yes\n' "$1"
+        else
+            printf '  %-16s no\n' "$1"
+        fi
+    }
+    check_header acl        sys/acl.h
+    check_header xattr      sys/xattr.h
+    check_header xxhash     xxhash.h
+    check_header zstd       zstd.h
+    check_header lz4        lz4.h
+    check_header openssl    openssl/md5.h
+    check_header zlib       zlib.h
+
+    echo
+    bold "Verifying the manpage generator..."
+    MD_OK=0
+    for m in cmarkgfm commonmark; do
+        if python3 -c "import $m" >/dev/null 2>&1; then
+            echo "  python3 module '$m' importable"
+            MD_OK=1
+            break
+        fi
+    done
+    if [ "$MD_OK" -eq 0 ]; then
+        if [ "$MINIMAL" -eq 1 ]; then
+            echo "  skipped (--minimal); build with ./configure --disable-md2man"
+        else
+            red "  neither cmarkgfm nor commonmark is importable."
+            echo "  Install one with:  python3 -mpip install --user commonmark"
+            echo "  ...or build with:  ./configure --disable-md2man"
+        fi
+    fi
 fi
 
-echo
-bold "Verifying optional libraries..."
-check_header() {
-    # $1 = human name, $2 = header path
-    if [ -e "/usr/include/$2" ] || \
-       find /usr/include -maxdepth 3 -name "$(basename "$2")" -print -quit \
-            2>/dev/null | grep -q .; then
-        printf '  %-16s yes\n' "$1"
+if [ "$WANT_AGI" -eq 1 ]; then
+    echo
+    bold "Verifying AGI..."
+    if [ -x /opt/agi/agi ]; then
+        printf '  %-16s %s\n' "agi" "/opt/agi/agi ($(agi_installed_version))"
     else
-        printf '  %-16s no\n' "$1"
+        printf '  %-16s %s\n' "agi" "MISSING"
     fi
-}
-check_header acl        sys/acl.h
-check_header xattr      sys/xattr.h
-check_header xxhash     xxhash.h
-check_header zstd       zstd.h
-check_header lz4        lz4.h
-check_header openssl    openssl/md5.h
-check_header zlib       zlib.h
 
-echo
-bold "Verifying the manpage generator..."
-MD_OK=0
-for m in cmarkgfm commonmark; do
-    if python3 -c "import $m" >/dev/null 2>&1; then
-        echo "  python3 module '$m' importable"
-        MD_OK=1
-        break
+    # The launcher settles for a JRE >= 11, but the GUI's jar is Java 19
+    # bytecode -- an older JVM starts and then dies on UnsupportedClassVersion.
+    if command -v java >/dev/null 2>&1; then
+        JAVA_MAJOR=$(java -version 2>&1 \
+            | sed -n '1s/.*version "\([0-9]\{1,\}\).*/\1/p')
+        JAVA_MAJOR=${JAVA_MAJOR:-0}
+        if [ "$JAVA_MAJOR" -ge 19 ]; then
+            printf '  %-16s java %s\n' "jre" "$JAVA_MAJOR"
+        else
+            printf '  %-16s java %s (too old, AGI needs 19+)\n' "jre" "$JAVA_MAJOR"
+        fi
+    else
+        printf '  %-16s %s\n' "jre" "MISSING"
     fi
-done
-if [ "$MD_OK" -eq 0 ]; then
-    if [ "$MINIMAL" -eq 1 ]; then
-        echo "  skipped (--minimal); build with ./configure --disable-md2man"
+
+    for lib in libgtk-3.so.0 libwebkit2gtk-4.1.so.0; do
+        if ldconfig -p 2>/dev/null | grep -q "$lib"; then
+            printf '  %-16s yes\n' "${lib%%.so*}"
+        else
+            printf '  %-16s no\n' "${lib%%.so*}"
+        fi
+    done
+
+    # Not fatal: AGI runs without adb, it just cannot see Android devices.
+    if command -v adb >/dev/null 2>&1; then
+        printf '  %-16s %s\n' "adb" "$(command -v adb)"
     else
-        red "  neither cmarkgfm nor commonmark is importable."
-        echo "  Install one with:  python3 -mpip install --user commonmark"
-        echo "  ...or build with:  ./configure --disable-md2man"
+        printf '  %-16s %s\n' "adb" "not installed (no Android devices)"
     fi
 fi
 
@@ -255,8 +676,9 @@ if [ ${#OPT_FAILED[@]} -ne 0 ]; then
     echo "The build will still work, with those features disabled."
 fi
 
-green "All required build dependencies are installed."
-cat <<'EOF'
+if [ "$WANT_RSYNC" -eq 1 ]; then
+    green "All required build dependencies are installed."
+    cat <<'EOF'
 
 Build rsync with either build system:
 
@@ -267,3 +689,32 @@ Run the test suite (needs the autoconf build's helper programs):
 
   make check
 EOF
+fi
+
+if [ "$WANT_AGI" -eq 1 ] && [ "$AGI_OK" -eq 1 ]; then
+    green "AGI $AGI_WANTED is installed."
+    if [ "$PM" = apt ]; then
+        cat <<'EOF'
+
+Run it from the desktop menu ("Android GPU Inspector") or:
+
+  /opt/agi/agi          # GUI      (--console to keep it in the terminal)
+  /opt/agi/gapit        # CLI
+
+Remove it with:  sudo apt-get remove agi
+EOF
+    else
+        cat <<'EOF'
+
+Run it from the desktop menu ("Android GPU Inspector") or:
+
+  agi                   # GUI, symlinked into /usr/local/bin
+  /opt/agi/gapit        # CLI
+
+Remove it with:  sudo rm -rf /opt/agi /usr/local/bin/agi \
+                     /usr/share/applications/google-agi.desktop \
+                     /usr/share/mime/packages/agi.xml
+EOF
+    fi
+    echo "Tracing an Android device needs adb and USB debugging enabled on it."
+fi