From b53937ffd307822bebbc1fa7a9dd7aabc2516a55 Mon Sep 17 00:00:00 2001 From: Michal Landsman Date: Wed, 12 Aug 2026 13:17:26 +0200 Subject: [PATCH] devops: telegram on both platforms, and flathub as the channel linux has left MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The native macOS cask on the Mac, deliberately not `telegram-desktop`. The Linux half is the awkward one: `telegram-desktop` was in the Ubuntu archive up to jammy and is gone from noble onward, and upstream publishes a tarball, a Snap and a Flatpak but no apt repo — so neither the archive rows nor the Discord hand-download shape applies. Flathub is the only channel left that still updates, which makes this the first flatpak here. It gets the same discipline as apt: Flathub's key ships inline as one base64 line of flathub.flatpakrepo, so the script decodes it, pins the fingerprint 6E5C05D979C76DAF93C081354184DD4D907A7CAE (cross-checked against the standalone flathub.gpg) and adds the remote from the verified local file — `remote-add ` would re-fetch the key and trust whatever came back. Two things fell out of it. `apt-get install` is now guarded, because a run whose only missing app is the flatpak would otherwise call it with no arguments; and the missing-package loop needs the empty-array-safe expansion, since bash 3.2 on the macOS CI leg cannot expand an empty one under `set -u`. --- Brewfile | 3 ++ os/ubuntu/README.md | 36 +++++++++++-- os/ubuntu/install-apps.sh | 99 +++++++++++++++++++++++++++++----- os/ubuntu/install-apps.test.sh | 62 +++++++++++++++++++-- 4 files changed, 179 insertions(+), 21 deletions(-) diff --git a/Brewfile b/Brewfile index f5428a8..8da2aeb 100644 --- a/Brewfile +++ b/Brewfile @@ -138,6 +138,9 @@ if OS.mac? cask "sublime-text" # Mesh VPN — the GUI app, the plain formula is CLI-only cask "tailscale-app" + # Messaging — the native macOS client, deliberately not the `telegram-desktop` + # cask. The Linux boxes get the Qt one from Flathub, see os/ubuntu. + cask "telegram" # Media player — opens the .mp4 and .m4a associations cask "vlc" # Video meetings and messaging diff --git a/os/ubuntu/README.md b/os/ubuntu/README.md index e2b52e4..cb23e69 100644 --- a/os/ubuntu/README.md +++ b/os/ubuntu/README.md @@ -26,6 +26,7 @@ cannot — from the vendors' own apt repos, except where the table says otherwis | Discord | `discord` | **none** — the vendor `.deb`, see below | | VLC | `vlc` | **none needed** — in the Ubuntu archive | | LibreOffice | `libreoffice` | **none needed** — in the Ubuntu archive | +| Telegram | `org.telegram.desktop` | **none** — Flathub, not apt at all, see below | Docker is the *engine*, not Docker Desktop: Desktop for Linux is a hand-download `.deb` with no repo behind it, and the engine is what @@ -53,6 +54,27 @@ knowing: apt will never update it, so `sudo apt-get remove discord` and another usual prompt), and this exception is per app, not a new rule — the hand-download apps below stay out. +Telegram is the one app that comes from neither apt nor a `.deb`. +`telegram-desktop` was in the Ubuntu archive up to jammy and is gone from noble +onward, and upstream publishes a tarball, a Snap and a Flatpak but no apt repo — +so unlike Discord there is not even a hand-download `.deb` to fall back on. +Flathub is what is left that still updates, which makes this the repo's first +flatpak: the script installs `flatpak` and `plasma-discover-backend-flatpak` from +the archive, adds the Flathub remote and pulls `org.telegram.desktop` system-wide. +The Discover backend is the part worth not skipping — without it a flatpak +updates only from the command line, which is exactly the trap Discord is in. + +Flathub's key is pinned like every apt key, by a slightly different route: it +ships inline as one base64 line of `flathub.flatpakrepo` rather than as a +download of its own, so the script decodes it, checks the fingerprint, and then +adds the remote **from the verified local file** rather than from the URL — +`flatpak remote-add ` would re-fetch the key and trust whatever came back, +which would leave the pin decorative. The test asserts both halves. + +macOS deliberately runs the other client: the [`Brewfile`](../../Brewfile) +installs the `telegram` cask, Telegram's native macOS app, rather than the Qt +`telegram-desktop` that Linux gets. Same account, different build, on purpose. + ### Not installable this way | App | Why | @@ -65,6 +87,11 @@ apps below stay out. | ZoomIt | Sysinternals ships it for Windows and macOS only, no Linux build | | Webex, Zed, JetBrains Toolbox | Linux builds exist, but as a hand-download `.deb`, an install script and a tarball respectively — none is an apt repo, so none gets updates through apt. Worth adding only deliberately | +Flathub being wired up now does not empty that table by itself. It was added for +the one app whose Linux build had no other channel left, and it stays that: an +app earns a flatpak once apt has been ruled out and the reason is written down +here, not because Flathub happens to carry it too. + `make apps` runs [`install-apps.sh`](install-apps.sh) itself, right after `brew bundle`, so there is nothing extra to remember on a new machine — it looks for `os/$ID/install-apps.sh` using the same `/etc/os-release` detection that @@ -98,10 +125,11 @@ debsig-signs the package, and apt refuses to unpack the second without a policy under `/etc/debsig/policies/` naming the key. [`install-apps.test.sh`](install-apps.test.sh) covers all of this with `apt`, -`dpkg`, `curl` and `gpg` stubbed and every root-owned path redirected into a -temp directory, so it runs on any machine — `make qa` includes it. The case it -exists for is the mismatched fingerprint: a pin that silently passes everything -would be worse than no pin at all. +`dpkg`, `curl`, `gpg` and `flatpak` stubbed and every root-owned path redirected +into a temp directory, so it runs on any machine — `make qa` includes it. The +case it exists for is the mismatched fingerprint, asserted for an apt key and for +Flathub's: a pin that silently passes everything would be worse than no pin at +all. Not `1password-cli` — the same apt repo carries it, but the `1password-cli` cask does ship a Linux build, so the [`Brewfile`](../../Brewfile) installs `op` on diff --git a/os/ubuntu/install-apps.sh b/os/ubuntu/install-apps.sh index a15cf33..6c81b28 100755 --- a/os/ubuntu/install-apps.sh +++ b/os/ubuntu/install-apps.sh @@ -11,6 +11,10 @@ # are not reachable this way — including the ones with no Linux build at all — # is written down in the README next to this file. # +# Apt is the channel; Flathub is the exception, for an app whose Linux build is +# real but reaches nobody through apt. Deliberately not a general escape hatch — +# an app goes there only once apt has been ruled out and the README says why. +# # Idempotent, and quiet when there is nothing to do: already-installed apps are # skipped before anything asks for root, so a re-run of `make apps` on a # provisioned machine neither reinstalls nor prompts for a password. @@ -41,6 +45,12 @@ STRIPE_KEY_FPR="6681D7C3D103DAC65D79C25EDEEBD57F917C83E3" # yearly and the primary is what apt verifies against, so the primary is pinned. GOOGLE_KEY_URL="https://dl.google.com/linux/linux_signing_key.pub" GOOGLE_KEY_FPR="EB4C1BFD4F042F6DDDCCEC917721F63BD38B4796" +# Flathub ships its key inside the .flatpakrepo rather than as a download of its +# own, so this pin is checked against what that file carries. Same primary key +# as the standalone https://dl.flathub.org/repo/flathub.gpg, which is how it was +# cross-checked; the subkey under it rotates, the primary is what signs. +FLATHUB_REPO_URL="https://dl.flathub.org/repo/flathub.flatpakrepo" +FLATHUB_KEY_FPR="6E5C05D979C76DAF93C081354184DD4D907A7CAE" # The package each app is identified by. Docker pulls in its plugins too, but # docker-ce is what "is Docker here?" comes down to. Two of them need no vendor @@ -49,6 +59,12 @@ GOOGLE_KEY_FPR="EB4C1BFD4F042F6DDDCCEC917721F63BD38B4796" PACKAGES=(1password sublime-text dbeaver-ce docker-ce tailscale discord google-chrome-stable vlc libreoffice stripe) +# The apps that come from Flathub instead, by app id. Telegram is the only one +# and the reason this half exists at all: it was dropped from the Ubuntu archive +# after jammy, and upstream publishes a tarball, a Snap and a Flatpak but no apt +# repo — so there is nothing for the machinery above to hang an app on. +FLATPAKS=(org.telegram.desktop) + installed() { [ "$(dpkg-query -W -f='${db:Status-Status}' "$1" 2>/dev/null)" = "installed" ] } @@ -58,12 +74,22 @@ for pkg in "${PACKAGES[@]}"; do installed "$pkg" || MISSING+=("$pkg") done -if [ ${#MISSING[@]} -eq 0 ]; then - echo "== distro apps: all ${#PACKAGES[@]} installed" +# Swallows "command not found" as well as "not installed", which is what a +# machine without flatpak yet should read as: the app is missing either way. +MISSING_FLATPAK=() +for app in "${FLATPAKS[@]}"; do + flatpak info "$app" >/dev/null 2>&1 || MISSING_FLATPAK+=("$app") +done + +if [ ${#MISSING[@]} -eq 0 ] && [ ${#MISSING_FLATPAK[@]} -eq 0 ]; then + echo "== distro apps: all $(( ${#PACKAGES[@]} + ${#FLATPAKS[@]} )) installed" exit 0 fi -echo "== distro apps: missing ${MISSING[*]}" +# Two lines rather than one joined list: either array can be empty here, and an +# empty one is not expandable under `set -u` on the bash the tests run under. +[ ${#MISSING[@]} -eq 0 ] || echo "== distro apps: missing ${MISSING[*]}" +[ ${#MISSING_FLATPAK[@]} -eq 0 ] || echo "== flatpaks: missing ${MISSING_FLATPAK[*]}" # Root is needed from here on. Re-exec rather than sudo per line, so the # password is asked for once and the whole run shares one timestamp. The marker @@ -88,12 +114,14 @@ CODENAME="${CODENAME:-$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_C TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT -# Fetch a signing key, refuse it unless the fingerprint matches, and leave the -# dearmoured copy at $TMP/.gpg for the caller. -fetch_key() { - local name="$1" url="$2" want="$3" got - curl -fsS "$url" -o "$TMP/$name.key" - got="$(gpg --show-keys --with-colons "$TMP/$name.key" | awk -F: '/^fpr:/ { print $10; exit }')" +# The pin itself: a key file is trusted only when its primary fingerprint is the +# one written down at the top. Its own function because Flathub's key arrives +# inside the .flatpakrepo rather than as a download, so apt and flatpak reach +# this check by different routes — and one copy of it is what keeps the two +# channels honest about the same thing. +check_fpr() { + local name="$1" file="$2" want="$3" got + got="$(gpg --show-keys --with-colons "$file" | awk -F: '/^fpr:/ { print $10; exit }')" if [ "$got" != "$want" ]; then echo "$name: key fingerprint mismatch" >&2 echo " expected $want" >&2 @@ -101,6 +129,14 @@ fetch_key() { echo "refusing to trust it - verify against the vendor's docs first" >&2 exit 1 fi +} + +# Fetch a signing key, refuse it unless the fingerprint matches, and leave the +# dearmoured copy at $TMP/.gpg for the caller. +fetch_key() { + local name="$1" url="$2" want="$3" + curl -fsS "$url" -o "$TMP/$name.key" + check_fpr "$name" "$TMP/$name.key" "$want" gpg --dearmor --yes --output "$TMP/$name.gpg" "$TMP/$name.key" } @@ -138,7 +174,11 @@ setup_1password() { } INSTALL=() -for pkg in "${MISSING[@]}"; do +# `${a[@]+"${a[@]}"}` rather than a plain `"${a[@]}"`: MISSING is legitimately +# empty on a run whose only missing app is the flatpak, and an empty array is +# not expandable under `set -u` on bash 3.2 — which is the bash the tests run +# under on macOS. The long form still quotes each element. +for pkg in ${MISSING[@]+"${MISSING[@]}"}; do echo "==> Configuring $pkg" case "$pkg" in 1password) @@ -201,15 +241,46 @@ for pkg in "${MISSING[@]}"; do esac done -echo "==> Installing ${INSTALL[*]}" -apt-get update -qq -apt-get install -y "${INSTALL[@]}" +# Flatpak is not on a Kubuntu install by default, and neither is the Discover +# backend — without that one a flatpak updates only from the command line, which +# is the same trap the Discord .deb is already in. Both come from the archive. +if [ ${#MISSING_FLATPAK[@]} -gt 0 ] && ! installed flatpak; then + echo "==> Configuring flatpak" + INSTALL+=(flatpak plasma-discover-backend-flatpak) +fi + +# Guarded, because a run whose only missing app is a flatpak leaves this empty +# and `apt-get install` with no arguments is an error, not a no-op. +if [ ${#INSTALL[@]} -gt 0 ]; then + echo "==> Installing ${INSTALL[*]}" + apt-get update -qq + apt-get install -y "${INSTALL[@]}" +fi + +if [ ${#MISSING_FLATPAK[@]} -gt 0 ]; then + echo "==> Configuring flathub" + curl -fsS "$FLATHUB_REPO_URL" -o "$TMP/flathub.flatpakrepo" + # The key is one base64 line of that ini file. Pull it out and hold it to the + # same pin as every apt key here: once the remote is added it authenticates + # every app and every update from it, which is the concern that made the + # fingerprints above worth pinning in the first place. + sed -n 's/^GPGKey=//p' "$TMP/flathub.flatpakrepo" | base64 -d >"$TMP/flathub.gpg" + check_fpr flathub "$TMP/flathub.gpg" "$FLATHUB_KEY_FPR" + # Added from the verified local file rather than from the URL, so the key + # that ends up trusted is the one just checked and not a second fetch of it. + flatpak remote-add --if-not-exists flathub "$TMP/flathub.flatpakrepo" + echo "==> Installing ${MISSING_FLATPAK[*]}" + flatpak install -y flathub "${MISSING_FLATPAK[@]}" +fi echo -echo "Done. Three things this script deliberately leaves to you:" +echo "Done. Four things this script deliberately leaves to you:" echo " - docker: 'usermod -aG docker \$USER' is what lets lazydocker talk to" echo " the socket without sudo, and it is root-equivalent - your call." echo " - tailscale: installed but not joined, run 'sudo tailscale up'." echo " - discord: no apt repo backs that .deb, so apt will never update it." echo " 'sudo apt-get remove discord' and re-run this to get the current one," echo " which is what an outdated client refusing to connect is telling you." +echo " - telegram: a flatpak, and one only reaches the app menu once the session" +echo " has read /etc/profile.d/flatpak.sh - log out and back in if it is not" +echo " there. 'flatpak run org.telegram.desktop' works right now either way." diff --git a/os/ubuntu/install-apps.test.sh b/os/ubuntu/install-apps.test.sh index 3508fb4..b3a9824 100644 --- a/os/ubuntu/install-apps.test.sh +++ b/os/ubuntu/install-apps.test.sh @@ -28,7 +28,9 @@ check() { if [ "$2" = "$3" ]; then ok "$1"; else echo " expected: $3" echo " actual: $2" fi; } -contains() { if grep -qF "$3" "$2" 2>/dev/null; then ok "$1"; else +# `--` because a pattern starting with one, like `--if-not-exists`, is otherwise +# read by grep as an option and the assertion silently fails on its own syntax. +contains() { if grep -qF -- "$3" "$2" 2>/dev/null; then ok "$1"; else fail "$1" echo " $2 has no line containing: $3" fi; } @@ -54,8 +56,24 @@ echo amd64' >"$BIN/dpkg" for a in "$@"; do [ "$prev" = "-o" ] && out="$a"; prev="$a"; done # The 1Password debsig policy is grepped for the key id, so echo one. case "$out" in *.pol) echo '' >"$out" ;; + # Flathub's key is one base64 line of the .flatpakrepo, and the script + # decodes it before checking the pin — so it has to survive a round trip. + *.flatpakrepo) printf 'Url=https://dl.flathub.org/repo/\nGPGKey=%s\n' \ + "$(printf key-material | base64)" >"$out" ;; *) echo "key-material" >"$out" ;; esac STUB + # FLATPAK_PRESENT is the app ids `flatpak info` should claim are installed. + # remote-add and install both land in files, so the case can assert that the + # remote came from the verified local copy and not from the URL. + cat >"$BIN/flatpak" <<-STUB + #!/usr/bin/env bash + case "\$1" in + info) for a in \$FLATPAK_PRESENT; do [ "\$a" = "\$2" ] && exit 0; done; exit 1 ;; + remote-add) shift; printf '%s\n' "\$@" >"$ROOT/flatpak-remote" ;; + install) shift; printf '%s\n' "\$@" | grep -v '^-y\$' >"$ROOT/flatpak-installed" ;; + esac + exit 0 + STUB cat >"$BIN/gpg" <<-STUB #!/usr/bin/env bash case "\$1" in @@ -86,7 +104,10 @@ FAKE_ROOT=1 exec "$@"' >"$BIN/sudo" chmod +x "$BIN"/* } -run() { PATH="$BIN:$PATH" CODENAME=noble INSTALLED="$1" FPR="$2" bash "$SCRIPT" 2>&1; } +# The flatpak defaults to present, so every apt case below is unaffected by it +# and only the flatpak cases have to say anything about it. +run() { PATH="$BIN:$PATH" CODENAME=noble INSTALLED="$1" FPR="$2" \ + FLATPAK_PRESENT="${3-org.telegram.desktop}" bash "$SCRIPT" 2>&1; } ALL="1password sublime-text dbeaver-ce docker-ce tailscale discord google-chrome-stable vlc libreoffice stripe" # The list minus one app, so a case can be "only this one is missing". @@ -96,7 +117,7 @@ echo "== every package already present" setup out="$(run "$ALL" deadbeef)" check "exits before doing anything" "$?" "0" -check "says so" "$(echo "$out" | tail -1)" "== distro apps: all 10 installed" +check "says so" "$(echo "$out" | tail -1)" "== distro apps: all 11 installed" if [ -f "$ROOT/apt-installed" ]; then fail "apt never ran"; else ok "apt never ran"; fi rm -rf "$ROOT" @@ -178,6 +199,41 @@ check "and neither adds a repo or a key" \ "$(test -d "$ROOT/etc/apt" -o -d "$ROOT/usr/share/keyrings" && echo yes || echo no)" "no" rm -rf "$ROOT" +echo +echo "== telegram, the first flatpak" +setup +# Every apt package present, so the only thing to do is the flatpak — which is +# also the run that proves apt is not called with an empty argument list. +out="$(run "$ALL" 6E5C05D979C76DAF93C081354184DD4D907A7CAE "")" +check "succeeds" "$?" "0" +check "apt is asked for flatpak and the Discover backend, nothing else" \ + "$(tr '\n' ' ' <"$ROOT/apt-installed")" "flatpak plasma-discover-backend-flatpak " +check "the app is installed from flathub" \ + "$(tr '\n' ' ' <"$ROOT/flatpak-installed")" "flathub org.telegram.desktop " +contains "the remote is added, and only if it is not there already" \ + "$ROOT/flatpak-remote" "--if-not-exists" +# The point of this one: `remote-add ` would re-fetch the key and trust +# whatever came back, making the pin two lines above decorative. +case "$(cat "$ROOT/flatpak-remote")" in +*https://*) fail "from the checked local copy, not the URL" ;; +*.flatpakrepo) ok "from the checked local copy, not the URL" ;; +*) fail "from the checked local copy, not the URL" ;; +esac +rm -rf "$ROOT" + +echo +echo "== flathub's key, when it is not the pinned one" +setup +out="$(run "$ALL" 0000000000000000000000000000000000000000 "")" +check "refuses to continue" "$?" "1" +case "$out" in *"flathub: key fingerprint mismatch"*) ok "says which key" ;; +*) fail "says which key" ;; esac +# apt already ran by then — flatpak itself is a legitimate install either way. +# What must not have happened is the remote being added or the app pulled from it. +if [ -f "$ROOT/flatpak-remote" ]; then fail "adds no remote"; else ok "adds no remote"; fi +if [ -f "$ROOT/flatpak-installed" ]; then fail "installs no app"; else ok "installs no app"; fi +rm -rf "$ROOT" + echo if [ "$FAILED" -eq 0 ]; then echo "all passed"; else echo "failures"; fi exit "$FAILED"