]> vilimpoc.org git repositories - dotfiles/blob - setup-linux.sh
dotfiles: record what VTune gives a standard user
[dotfiles] / setup-linux.sh
1 #!/bin/bash
2 #
3 # setup-linux.sh -- install the rsync build toolchain and AGI.
4 #
5 # Tested on Ubuntu 22.04.5 LTS and 26.04 LTS, and on CachyOS.  Covers both
6 # rsync build systems: the autoconf one (./configure && make) and the CMake
7 # one (CMakeLists.txt), plus the optional feature libraries and the manpage
8 # generator.  Also installs AGI (Android GPU Inspector) from the latest
9 # google/agi release: the .deb on Debian/Ubuntu, the .zip on Arch/CachyOS.
10 #
11 # Usage:
12 #     ./setup-linux.sh              # rsync required + optional, then AGI
13 #     ./setup-linux.sh --minimal    # rsync required only, no AGI
14 #     ./setup-linux.sh --no-agi     # rsync deps only
15 #     ./setup-linux.sh --agi-only   # AGI and its dependencies only
16 #     ./setup-linux.sh --dry-run    # show what would be installed
17 #
18 # AGI_VERSION=3.3.3 ./setup-linux.sh pins a release instead of asking GitHub
19 # for the latest one.
20 #
21 # Safe to re-run: the package managers skip anything already present, and AGI
22 # is re-downloaded only when the installed version is not the wanted one.
23
24 set -uo pipefail
25
26 MINIMAL=0
27 DRY_RUN=0
28 WANT_AGI=1
29 WANT_RSYNC=1
30
31 for arg in "$@"; do
32     case "$arg" in
33     --minimal)  MINIMAL=1; WANT_AGI=0 ;;
34     --dry-run)  DRY_RUN=1 ;;
35     --no-agi)   WANT_AGI=0 ;;
36     --agi-only) WANT_RSYNC=0; WANT_AGI=1 ;;
37     -h|--help)
38         sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//'
39         exit 0 ;;
40     *)
41         echo "setup-linux.sh: unknown option '$arg' (try --help)" >&2
42         exit 2 ;;
43     esac
44 done
45
46 # ---------------------------------------------------------------- helpers
47
48 red()   { printf '\033[31m%s\033[0m\n' "$*"; }
49 green() { printf '\033[32m%s\033[0m\n' "$*"; }
50 bold()  { printf '\033[1m%s\033[0m\n' "$*"; }
51
52 die() { red "ERROR: $*"; exit 1; }
53
54 # ------------------------------------------------- package manager wrapper
55
56 # Two families are supported: apt (Debian/Ubuntu) and pacman (Arch/CachyOS).
57 PM=""
58 if command -v apt-get >/dev/null 2>&1; then
59     PM=apt
60 elif command -v pacman >/dev/null 2>&1; then
61     PM=pacman
62 else
63     die "found neither apt-get nor pacman -- unsupported distribution."
64 fi
65
66 pkg_available() {
67     case "$PM" in
68     apt)
69         local cand
70         cand=$(apt-cache policy "$1" 2>/dev/null | awk '/Candidate:/{print $2}')
71         [ -n "$cand" ] && [ "$cand" != "(none)" ] ;;
72     pacman)
73         pacman -Si "$1" >/dev/null 2>&1 ;;
74     esac
75 }
76
77 # Echo the first name the distro actually ships.  Package names drift between
78 # releases (libgtk-3-0 became libgtk-3-0t64 in Ubuntu 24.04, and the JDK
79 # version in the archive moves every six months), so nothing is hard-coded.
80 pick_pkg() {
81     local p
82     for p in "$@"; do
83         if pkg_available "$p"; then
84             echo "$p"
85             return 0
86         fi
87     done
88     return 1
89 }
90
91 pkg_install() {
92     [ $# -eq 0 ] && return 0
93     case "$PM" in
94     apt)    apt-get install -y "$@" ;;
95     pacman) pacman -S --needed --noconfirm "$@" ;;
96     esac
97 }
98
99 # ------------------------------------------------------- sanity checks
100
101 DISTRO="unknown"
102 RELEASE="unknown"
103 if [ -r /etc/os-release ]; then
104     # shellcheck disable=SC1091  # runtime file, not available to the linter
105     . /etc/os-release
106     DISTRO="${ID:-unknown}"
107     RELEASE="${VERSION_ID:-unknown}"
108 fi
109
110 bold "rsync build dependencies and AGI"
111 echo "  distro ......... ${PRETTY_NAME:-$DISTRO $RELEASE}"
112 echo "  architecture ... $(uname -m)"
113 echo "  packages ....... $PM"
114
115 case "$DISTRO" in
116 ubuntu|debian|linuxmint|pop|arch|cachyos|endeavouros|manjaro) ;;
117 *)  echo
118     red "Warning: '$DISTRO' is untested; assuming it behaves like a $PM distro."
119     echo "Continuing anyway -- package names may differ."
120     ;;
121 esac
122
123 # AGI ships x86-64 binaries only.
124 case "$(uname -m)" in
125 x86_64|amd64) ;;
126 *)  if [ "$WANT_AGI" -eq 1 ]; then
127         red "Warning: AGI publishes x86-64 Linux builds only; skipping it."
128         WANT_AGI=0
129     fi
130     ;;
131 esac
132
133 # The package managers need root.  Re-exec under sudo rather than sprinkling
134 # it around, so that a single password prompt covers the whole run.
135 if [ "$(id -u)" -ne 0 ] && [ "$DRY_RUN" -eq 0 ]; then
136     command -v sudo >/dev/null 2>&1 \
137         || die "not running as root and sudo is not installed."
138     echo
139     echo "Re-running under sudo..."
140     # Plain sudo, not "sudo -E": some sudoers configs refuse to preserve the
141     # environment, and the exports below happen in the root instance anyway.
142     exec sudo AGI_VERSION="${AGI_VERSION:-}" "$0" "$@"
143 fi
144
145 export DEBIAN_FRONTEND=noninteractive
146 export NEEDRESTART_MODE=a          # don't prompt about restarting services
147
148 # ------------------------------------------------------- rsync packages
149
150 REQUIRED=()
151 OPTIONAL=()
152
153 if [ "$WANT_RSYNC" -eq 1 ]; then
154     case "$PM" in
155     apt)
156         # Needed to build rsync at all.
157         REQUIRED=(
158             build-essential     # gcc, g++, make, libc headers
159             gawk                # rsync needs a modern awk for its code generators
160             autoconf            # ./configure is generated from configure.ac
161             automake
162             python3             # manpage + header generators, and the test suite
163             cmake               # the CMake build
164             ninja-build         # ...and its default generator
165             git                 # version stamping (git-version.h)
166             pkg-config
167         )
168
169         # Optional: each one switches on an rsync feature.  Missing ones only
170         # mean a smaller feature set, never a failed build.
171         OPTIONAL=(
172             acl libacl1-dev         # --acls  (helper tools also used by the tests)
173             attr libattr1-dev       # --xattrs (likewise)
174             libxxhash-dev           # xxhash checksums (becomes the default)
175             libzstd-dev             # zstd compression (becomes the default)
176             liblz4-dev              # lz4 compression
177             libssl-dev              # OpenSSL MD4/MD5
178             zlib1g-dev              # for cmake -DRSYNC_EXTERNAL_ZLIB=ON
179             libpopt-dev             # for ./configure --with-included-popt=no
180         )
181         ;;
182     pacman)
183         # Arch splits nothing out into -dev packages: the headers ship with
184         # the library, and base-devel covers the compiler toolchain.
185         REQUIRED=(
186             base-devel          # gcc, make, and friends
187             gawk
188             autoconf
189             automake
190             python
191             cmake
192             ninja
193             git
194             pkgconf
195         )
196         OPTIONAL=(
197             acl                     # --acls
198             attr                    # --xattrs
199             xxhash                  # xxhash checksums (becomes the default)
200             zstd                    # zstd compression (becomes the default)
201             lz4                     # lz4 compression
202             openssl                 # OpenSSL MD4/MD5
203             zlib                    # for cmake -DRSYNC_EXTERNAL_ZLIB=ON
204             popt                    # for ./configure --with-included-popt=no
205         )
206         ;;
207     esac
208
209     # Development tooling: not needed to build rsync, but useful when working
210     # on its shell scripts -- including this one, which shellcheck keeps honest.
211     #
212     # ffmpeg and adb are here for the same reason: they are common across the
213     # projects on these machines rather than specific to any one of them, so
214     # installing them once here means a per-project setup script only has to
215     # CHECK for them instead of carrying its own package-manager logic.  What
216     # stays out of this script on purpose is anything project-shaped -- no
217     # virtualenvs, no per-repo Python packages, no test tooling.  Those belong
218     # next to the repo that needs them (e.g. RAWcorder's setup-linux-tests.sh).
219     #
220     #   ffmpeg        ffmpeg + ffprobe, for building and probing test media
221     #   adb           reaches Android devices.  AGI installs it too, but only
222     #                 on an --agi run, and it is worth having either way.
223     #   python3-venv  Debian and Ubuntu split venv and ensurepip out of python3,
224     #                 so `python3 -m venv` fails on a stock Ubuntu without it.
225     #                 Arch bundles both into `python`, so there is nothing to add.
226     OPTIONAL+=(shellcheck ffmpeg)
227     case "$PM" in
228     apt)
229         COMMON_PKG=$(pick_pkg python3-venv) && OPTIONAL+=("$COMMON_PKG")
230         COMMON_PKG=$(pick_pkg adb android-sdk-platform-tools) && OPTIONAL+=("$COMMON_PKG")
231         ;;
232     pacman)
233         COMMON_PKG=$(pick_pkg android-tools) && OPTIONAL+=("$COMMON_PKG")
234         ;;
235     esac
236
237     # The manpages need one of two python3 markdown libraries; upstream prefers
238     # cmarkgfm.  Pick whichever this release actually offers.
239     case "$PM" in
240     apt)    MARKDOWN_PKG=$(pick_pkg python3-cmarkgfm python3-commonmark) ;;
241     pacman) MARKDOWN_PKG=$(pick_pkg python-cmarkgfm python-commonmark) ;;
242     esac
243     if [ -n "$MARKDOWN_PKG" ]; then
244         OPTIONAL+=("$MARKDOWN_PKG")
245     fi
246
247     if [ "$MINIMAL" -eq 1 ]; then
248         OPTIONAL=()
249     fi
250 fi
251
252 # ------------------------------------------------------- AGI packages
253
254 # AGI is a Java/SWT desktop app plus a set of Go binaries.  The .deb declares
255 # "Depends: openjdk-22-jre, libgtk-3-0, libwebkit2gtk-4.1-0", but Ubuntu has
256 # never packaged openjdk-22 -- so pick a JRE that exists instead.  The GUI's
257 # class files are Java 19 bytecode, so anything from 21 up runs it; the launcher
258 # itself only insists on 11.  adb is not a hard dependency, but without it AGI
259 # cannot see Android devices ("adb could not be found from ANDROID_HOME or PATH").
260 AGI_DEPS=()
261 AGI_JRE=""
262 AGI_GTK=""
263 AGI_WEBKIT=""
264 AGI_ADB=""
265
266 if [ "$WANT_AGI" -eq 1 ]; then
267     case "$PM" in
268     apt)
269         AGI_JRE=$(pick_pkg openjdk-21-jre openjdk-25-jre openjdk-24-jre \
270                            openjdk-23-jre openjdk-22-jre openjdk-26-jre)
271         AGI_GTK=$(pick_pkg libgtk-3-0t64 libgtk-3-0)
272         AGI_WEBKIT=$(pick_pkg libwebkit2gtk-4.1-0)
273         AGI_ADB=$(pick_pkg adb android-sdk-platform-tools)
274         # curl fetches the release, binutils' ar rewrites the .deb's control
275         # member, ca-certificates lets curl trust github.com.
276         AGI_DEPS=(curl ca-certificates binutils xz-utils unzip)
277         ;;
278     pacman)
279         AGI_JRE=$(pick_pkg jre-openjdk jre21-openjdk)
280         AGI_GTK=$(pick_pkg gtk3)
281         AGI_WEBKIT=$(pick_pkg webkit2gtk-4.1)
282         AGI_ADB=$(pick_pkg android-tools)
283         AGI_DEPS=(curl unzip)
284         ;;
285     esac
286
287     for p in "$AGI_GTK" "$AGI_WEBKIT" "$AGI_ADB"; do
288         [ -n "$p" ] && AGI_DEPS+=("$p")
289     done
290
291     # A JRE already on the box is good enough if it is new enough to load the
292     # GUI's Java 19 class files.  On apt we install one anyway: the .deb's
293     # rewritten Depends line has to name a package dpkg knows about.
294     JAVA_MAJOR=0
295     if command -v java >/dev/null 2>&1; then
296         JAVA_MAJOR=$(java -version 2>&1 \
297             | sed -n '1s/.*version "\([0-9]\{1,\}\).*/\1/p')
298         JAVA_MAJOR=${JAVA_MAJOR:-0}
299     fi
300     if [ -n "$AGI_JRE" ] && { [ "$PM" = apt ] || [ "$JAVA_MAJOR" -lt 19 ]; }; then
301         AGI_DEPS+=("$AGI_JRE")
302     fi
303 fi
304
305 # ------------------------------------------------------- AGI release info
306
307 # Used only when GitHub cannot be reached; the release that this script was
308 # last checked against.
309 AGI_FALLBACK_VERSION=3.3.3
310 AGI_WANTED=""
311 AGI_INSTALLED=""
312
313 agi_installed_version() {
314     [ -r /opt/agi/build.properties ] || return 1
315     awk -F= '
316         /^Version\.Major/ { maj = $2 }
317         /^Version\.Minor/ { min = $2 }
318         /^Version\.Micro/ { mic = $2 }
319         END { if (maj == "") exit 1; print maj "." min "." mic }
320     ' /opt/agi/build.properties | tr -d ' \r'
321 }
322
323 agi_latest_version() {
324     command -v curl >/dev/null 2>&1 || return 1
325     curl -fsSL --retry 3 --max-time 20 \
326         https://api.github.com/repos/google/agi/releases/latest 2>/dev/null \
327         | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"v\{0,1\}\([^"]*\)".*/\1/p' \
328         | head -1
329 }
330
331 if [ "$WANT_AGI" -eq 1 ]; then
332     AGI_WANTED="${AGI_VERSION:-}"
333     if [ -z "$AGI_WANTED" ]; then
334         AGI_WANTED=$(agi_latest_version)
335     fi
336     if [ -z "$AGI_WANTED" ]; then
337         red "Could not reach the GitHub releases API; falling back to AGI $AGI_FALLBACK_VERSION."
338         AGI_WANTED="$AGI_FALLBACK_VERSION"
339     fi
340     AGI_INSTALLED=$(agi_installed_version) || AGI_INSTALLED=""
341 fi
342
343 AGI_BASE_URL="https://github.com/google/agi/releases/download"
344
345 # ------------------------------------------------------------- dry run
346
347 if [ "$DRY_RUN" -eq 1 ]; then
348     if [ "$WANT_RSYNC" -eq 1 ]; then
349         echo
350         bold "Would install (rsync, required):"
351         printf '  %s\n' "${REQUIRED[@]}"
352         if [ ${#OPTIONAL[@]} -gt 0 ]; then
353             bold "Would install (rsync, optional):"
354             printf '  %s\n' "${OPTIONAL[@]}"
355         fi
356     fi
357     if [ "$WANT_AGI" -eq 1 ]; then
358         echo
359         bold "Would install (AGI dependencies):"
360         printf '  %s\n' "${AGI_DEPS[@]}"
361         bold "Would install AGI $AGI_WANTED:"
362         if [ "$PM" = apt ]; then
363             echo "  $AGI_BASE_URL/v$AGI_WANTED/agi-$AGI_WANTED-linux.deb"
364             echo "  via dpkg, with Depends retargeted at ${AGI_JRE:-the installed JRE}"
365         else
366             echo "  $AGI_BASE_URL/v$AGI_WANTED/agi-$AGI_WANTED-linux.zip"
367             echo "  unpacked into /opt/agi, symlinked as /usr/local/bin/agi"
368         fi
369         [ -n "$AGI_INSTALLED" ] && echo "  (currently installed: $AGI_INSTALLED)"
370     fi
371     exit 0
372 fi
373
374 # ------------------------------------------------------------- install
375
376 echo
377 case "$PM" in
378 apt)
379     bold "Updating package lists..."
380     if ! apt-get update -qq; then
381         red "apt-get update failed -- continuing with the cached lists."
382     fi
383     ;;
384 pacman)
385     # No "pacman -Sy" here on purpose: refreshing the database without also
386     # upgrading is how Arch systems end up half-broken.  If a package fetch
387     # 404s below, the sync database is stale -- run "pacman -Syu" first.
388     bold "Using the existing pacman database (run 'pacman -Syu' if it is stale)."
389     ;;
390 esac
391
392 OPT_FAILED=()
393
394 if [ "$WANT_RSYNC" -eq 1 ]; then
395     echo
396     bold "Installing required packages..."
397     if ! pkg_install "${REQUIRED[@]}"; then
398         # Fall back to one-at-a-time so the failing package is obvious.
399         red "Bulk install failed; retrying individually to find the culprit..."
400         FAILED=()
401         for p in "${REQUIRED[@]}"; do
402             pkg_install "$p" || FAILED+=("$p")
403         done
404         [ ${#FAILED[@]} -eq 0 ] || die "could not install: ${FAILED[*]}"
405     fi
406
407     if [ ${#OPTIONAL[@]} -gt 0 ]; then
408         echo
409         bold "Installing optional packages..."
410         if ! pkg_install "${OPTIONAL[@]}"; then
411             red "Bulk install failed; retrying individually..."
412             for p in "${OPTIONAL[@]}"; do
413                 pkg_install "$p" || OPT_FAILED+=("$p")
414             done
415         fi
416     fi
417 fi
418
419 # ----------------------------------------------------------------- AGI
420
421 # The .desktop and MIME files the .deb ships, replayed for the .zip install so
422 # both routes leave the same system behind.
423 agi_desktop_files() {
424     cat >/usr/share/applications/google-agi.desktop <<'DESKTOP'
425 [Desktop Entry]
426 Encoding=UTF-8
427 Type=Application
428 Name=Android GPU Inspector
429 Comment=Android GPU Inspector
430 Categories=Development;Debugger;Graphics;3DGraphics
431 Icon=/opt/agi/icon.png
432 Exec=/opt/agi/agi %f
433 Terminal=false
434 MimeType=application/x-gfxtrace;application/x-perfetto
435 DESKTOP
436
437     mkdir -p /usr/share/mime/packages
438     cat >/usr/share/mime/packages/agi.xml <<'MIME'
439 <?xml version="1.0" encoding="UTF-8"?>
440 <mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
441    <mime-type type="application/x-gfxtrace">
442      <comment>Graphics API Trace</comment>
443      <glob pattern="*.gfxtrace"/>
444    </mime-type>
445    <mime-type type="application/x-perfetto">
446      <comment>System Profile Trace</comment>
447      <glob pattern="*.perfetto"/>
448    </mime-type>
449 </mime-info>
450 MIME
451
452     command -v update-mime-database >/dev/null 2>&1 \
453         && update-mime-database /usr/share/mime >/dev/null 2>&1
454     command -v update-desktop-database >/dev/null 2>&1 \
455         && update-desktop-database /usr/share/applications >/dev/null 2>&1
456     return 0
457 }
458
459 agi_download() {
460     # $1 = url, $2 = destination file
461     echo "  fetching $(basename "$1")..."
462     curl -fL --retry 3 --progress-bar -o "$2" "$1"
463 }
464
465 # Every dependency the .deb names has to exist in the archive, or dpkg will
466 # refuse the package.  openjdk-22-jre does not exist in any Ubuntu release,
467 # so rewrite that line to the JRE we just installed.  Only the control member
468 # is rebuilt -- unpacking and recompressing the 230MB payload with dpkg-deb
469 # would take minutes and change nothing.
470 deb_depends_satisfiable() {
471     local dep
472     while IFS= read -r dep; do
473         dep=$(echo "$dep" | sed 's/(.*)//; s/^[[:space:]]*//; s/[[:space:]]*$//')
474         [ -z "$dep" ] && continue
475         pkg_available "$dep" || return 1
476     done < <(dpkg-deb -f "$1" Depends 2>/dev/null | tr ',' '\n')
477     return 0
478 }
479
480 rewrite_deb_depends() {
481     # $1 = .deb, $2 = new Depends line, $3 = scratch directory
482     local deb="$1" depends="$2" work="$3/control"
483     command -v ar >/dev/null 2>&1 || return 1
484     rm -rf "$work"
485     mkdir -p "$work/c" || return 1
486     ( cd "$work" && ar x "$deb" control.tar.xz ) || return 1
487     tar xf "$work/control.tar.xz" -C "$work/c" || return 1
488     sed -i "s|^Depends:.*|Depends: $depends|" "$work/c/control" || return 1
489     rm -f "$work/control.tar.xz"
490     ( cd "$work/c" && tar cJf ../control.tar.xz . ) || return 1
491     ( cd "$work" && ar r "$deb" control.tar.xz ) || return 1
492     # Make sure dpkg still understands the archive we just edited.
493     dpkg-deb -f "$deb" Depends >/dev/null 2>&1 || return 1
494 }
495
496 install_agi_deb() {
497     # Split across two 'local's: a value assigned in the same 'local' as the
498     # variable that uses it has not taken effect yet (SC2318).
499     local ver="$1" tmp="$2"
500     local deb="$tmp/agi.deb" depends
501
502     agi_download "$AGI_BASE_URL/v$ver/agi-$ver-linux.deb" "$deb" || return 1
503
504     if ! deb_depends_satisfiable "$deb"; then
505         depends="${AGI_JRE:-default-jre}"
506         [ -n "$AGI_GTK" ]    && depends="$depends, $AGI_GTK"
507         [ -n "$AGI_WEBKIT" ] && depends="$depends, $AGI_WEBKIT"
508         echo "  retargeting the package's Depends at: $depends"
509         rewrite_deb_depends "$deb" "$depends" "$tmp" || {
510             red "  could not rewrite the .deb's dependencies."
511             return 1
512         }
513     fi
514
515     if ! dpkg -i "$deb"; then
516         red "  dpkg reported a problem; asking apt to resolve it..."
517         apt-get -f install -y || return 1
518         dpkg -i "$deb" || return 1
519     fi
520 }
521
522 install_agi_zip() {
523     local ver="$1" tmp="$2"
524
525     agi_download "$AGI_BASE_URL/v$ver/agi-$ver-linux.zip" "$tmp/agi.zip" || return 1
526     unzip -q "$tmp/agi.zip" -d "$tmp/unpacked" || return 1
527     [ -x "$tmp/unpacked/agi/agi" ] || {
528         red "  the zip did not contain agi/agi -- leaving /opt/agi alone."
529         return 1
530     }
531
532     # Only ever replace a directory that looks like a previous AGI install.
533     if [ -e /opt/agi ] && [ ! -e /opt/agi/build.properties ]; then
534         red "  /opt/agi exists but is not an AGI install; refusing to replace it."
535         return 1
536     fi
537
538     rm -rf /opt/agi
539     mv "$tmp/unpacked/agi" /opt/agi || return 1
540     chown -R root:root /opt/agi
541     ln -sf /opt/agi/agi /usr/local/bin/agi
542     agi_desktop_files
543 }
544
545 AGI_OK=0
546 if [ "$WANT_AGI" -eq 1 ]; then
547     echo
548     bold "Installing AGI dependencies..."
549     if ! pkg_install "${AGI_DEPS[@]}"; then
550         red "Bulk install failed; retrying individually..."
551         for p in "${AGI_DEPS[@]}"; do
552             pkg_install "$p" || OPT_FAILED+=("$p")
553         done
554     fi
555
556     echo
557     if [ -n "$AGI_INSTALLED" ] && [ "$AGI_INSTALLED" = "$AGI_WANTED" ]; then
558         bold "AGI $AGI_INSTALLED is already installed in /opt/agi."
559         AGI_OK=1
560     else
561         bold "Installing AGI $AGI_WANTED..."
562         [ -n "$AGI_INSTALLED" ] && echo "  replacing the installed $AGI_INSTALLED"
563         AGI_TMP=$(mktemp -d "${TMPDIR:-/tmp}/agi-install.XXXXXX") \
564             || die "could not create a temporary directory."
565         # 85MB of .deb or .zip; do not leave it behind on any exit path.
566         trap 'rm -rf "$AGI_TMP"' EXIT
567
568         if [ "$PM" = apt ]; then
569             if install_agi_deb "$AGI_WANTED" "$AGI_TMP"; then
570                 AGI_OK=1
571             else
572                 red "  .deb install failed; falling back to the zip release."
573                 install_agi_zip "$AGI_WANTED" "$AGI_TMP" && AGI_OK=1
574             fi
575         else
576             install_agi_zip "$AGI_WANTED" "$AGI_TMP" && AGI_OK=1
577         fi
578
579         rm -rf "$AGI_TMP"
580         trap - EXIT
581         [ "$AGI_OK" -eq 1 ] || red "AGI installation failed."
582     fi
583 fi
584
585 # --------------------------------------------------------------- verify
586
587 MISSING=()
588 check_cmd() {
589     if command -v "$1" >/dev/null 2>&1; then
590         printf '  %-16s %s\n' "$1" "$(command -v "$1")"
591     else
592         printf '  %-16s %s\n' "$1" "MISSING"
593         MISSING+=("$1")
594     fi
595 }
596
597 if [ "$WANT_RSYNC" -eq 1 ]; then
598     echo
599     bold "Verifying the toolchain..."
600
601     for c in gcc g++ make gawk autoconf automake cmake ninja python3 git; do
602         check_cmd "$c"
603     done
604
605     # None of these is required to build rsync, so report them without failing
606     # the run.  ffprobe has no package of its own -- it ships inside ffmpeg --
607     # so it is checked rather than installed, which also catches a stripped-down
608     # distro build that leaves it out.
609     for c in shellcheck ffmpeg ffprobe adb; do
610         if command -v "$c" >/dev/null 2>&1; then
611             printf '  %-16s %s\n' "$c" "$(command -v "$c")"
612         else
613             printf '  %-16s %s\n' "$c" "not installed (optional)"
614         fi
615     done
616
617     echo
618     bold "Verifying optional libraries..."
619     check_header() {
620         # $1 = human name, $2 = header path
621         if [ -e "/usr/include/$2" ] || \
622            find /usr/include -maxdepth 3 -name "$(basename "$2")" -print -quit \
623                 2>/dev/null | grep -q .; then
624             printf '  %-16s yes\n' "$1"
625         else
626             printf '  %-16s no\n' "$1"
627         fi
628     }
629     check_header acl        sys/acl.h
630     check_header xattr      sys/xattr.h
631     check_header xxhash     xxhash.h
632     check_header zstd       zstd.h
633     check_header lz4        lz4.h
634     check_header openssl    openssl/md5.h
635     check_header zlib       zlib.h
636
637     echo
638     bold "Verifying the manpage generator..."
639     MD_OK=0
640     for m in cmarkgfm commonmark; do
641         if python3 -c "import $m" >/dev/null 2>&1; then
642             echo "  python3 module '$m' importable"
643             MD_OK=1
644             break
645         fi
646     done
647     if [ "$MD_OK" -eq 0 ]; then
648         if [ "$MINIMAL" -eq 1 ]; then
649             echo "  skipped (--minimal); build with ./configure --disable-md2man"
650         else
651             red "  neither cmarkgfm nor commonmark is importable."
652             echo "  Install one with:  python3 -mpip install --user commonmark"
653             echo "  ...or build with:  ./configure --disable-md2man"
654         fi
655     fi
656 fi
657
658 if [ "$WANT_AGI" -eq 1 ]; then
659     echo
660     bold "Verifying AGI..."
661     if [ -x /opt/agi/agi ]; then
662         printf '  %-16s %s\n' "agi" "/opt/agi/agi ($(agi_installed_version))"
663     else
664         printf '  %-16s %s\n' "agi" "MISSING"
665     fi
666
667     # The launcher settles for a JRE >= 11, but the GUI's jar is Java 19
668     # bytecode -- an older JVM starts and then dies on UnsupportedClassVersion.
669     if command -v java >/dev/null 2>&1; then
670         JAVA_MAJOR=$(java -version 2>&1 \
671             | sed -n '1s/.*version "\([0-9]\{1,\}\).*/\1/p')
672         JAVA_MAJOR=${JAVA_MAJOR:-0}
673         if [ "$JAVA_MAJOR" -ge 19 ]; then
674             printf '  %-16s java %s\n' "jre" "$JAVA_MAJOR"
675         else
676             printf '  %-16s java %s (too old, AGI needs 19+)\n' "jre" "$JAVA_MAJOR"
677         fi
678     else
679         printf '  %-16s %s\n' "jre" "MISSING"
680     fi
681
682     for lib in libgtk-3.so.0 libwebkit2gtk-4.1.so.0; do
683         if ldconfig -p 2>/dev/null | grep -q "$lib"; then
684             printf '  %-16s yes\n' "${lib%%.so*}"
685         else
686             printf '  %-16s no\n' "${lib%%.so*}"
687         fi
688     done
689
690     # Not fatal: AGI runs without adb, it just cannot see Android devices.
691     if command -v adb >/dev/null 2>&1; then
692         printf '  %-16s %s\n' "adb" "$(command -v adb)"
693     else
694         printf '  %-16s %s\n' "adb" "not installed (no Android devices)"
695     fi
696 fi
697
698 # ---------------------------------------------------------------- done
699
700 echo
701 if [ ${#MISSING[@]} -ne 0 ]; then
702     red "Missing required tools: ${MISSING[*]}"
703     exit 1
704 fi
705
706 if [ ${#OPT_FAILED[@]} -ne 0 ]; then
707     red "Optional packages that failed to install: ${OPT_FAILED[*]}"
708     echo "The build will still work, with those features disabled."
709 fi
710
711 if [ "$WANT_RSYNC" -eq 1 ]; then
712     green "All required build dependencies are installed."
713     cat <<'EOF'
714
715 Build rsync with either build system:
716
717   autoconf:   ./configure && make
718   CMake:      cmake -B build -G Ninja && cmake --build build
719
720 Run the test suite (needs the autoconf build's helper programs):
721
722   make check
723 EOF
724 fi
725
726 if [ "$WANT_AGI" -eq 1 ] && [ "$AGI_OK" -eq 1 ]; then
727     green "AGI $AGI_WANTED is installed."
728     if [ "$PM" = apt ]; then
729         cat <<'EOF'
730
731 Run it from the desktop menu ("Android GPU Inspector") or:
732
733   /opt/agi/agi          # GUI      (--console to keep it in the terminal)
734   /opt/agi/gapit        # CLI
735
736 Remove it with:  sudo apt-get remove agi
737 EOF
738     else
739         cat <<'EOF'
740
741 Run it from the desktop menu ("Android GPU Inspector") or:
742
743   agi                   # GUI, symlinked into /usr/local/bin
744   /opt/agi/gapit        # CLI
745
746 Remove it with:  sudo rm -rf /opt/agi /usr/local/bin/agi \
747                      /usr/share/applications/google-agi.desktop \
748                      /usr/share/mime/packages/agi.xml
749 EOF
750     fi
751     echo "Tracing an Android device needs adb and USB debugging enabled on it."
752 fi