Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Brewfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 32 additions & 4 deletions os/ubuntu/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <url>` 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 |
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
99 changes: 85 additions & 14 deletions os/ubuntu/install-apps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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" ]
}
Expand All @@ -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
Expand All @@ -88,19 +114,29 @@ 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/<name>.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
echo " got ${got:-<none>}" >&2
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/<name>.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"
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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."
62 changes: 59 additions & 3 deletions os/ubuntu/install-apps.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -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 '<Origin id="AC2D62742012EA22"/>' >"$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
Expand Down Expand Up @@ -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".
Expand All @@ -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"

Expand Down Expand Up @@ -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 <url>` 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"
Loading