The box this was written on had a half-finished install: vscode.repo
present under /etc/yum.repos.d, `code` never installed, and nothing in
this repo recording that VS Code was meant to be there. Microsoft's
rpm/deb would also need root, write outside $HOME, and be wiped by a
Kasm redeploy. Upstream's portable tarball is the same build with none
of that, so the module keeps bin/'s contract: no sudo, nothing outside
$HOME, pinned rather than floating.
The Chromium sandbox survives a userland install — the bundled
chrome-sandbox is not setuid, so Electron falls back to the
unprivileged user-namespace sandbox. Verified by launching without
--no-sandbox, so dropping root costs nothing.
Adds install_tarball_tree to lib/fetch.sh: install_tarball_bin picks a
single binary out of an archive, and VS Code only runs from an intact
Electron tree. It stages beside the target and swaps in after
extraction succeeds, so an interrupted run leaves the old version
intact.
Two things the verification caught rather than the design:
- argv.json lives at ~/.vscode/argv.json (product.json's
dataFolderName), not under the user-data-dir. A copy written
anywhere else is read by nothing, so the first attempt at
suppressing the keyring prompt silently did nothing.
- Run as root, `code --version` prints a "super user" warning
instead of the version and still exits 0, so a bare run-check
never matches and would re-download 325MB on every bootstrap.
Root falls back to the .versions stamp.
Sets password-store=basic in argv.json so Electron's safeStorage stops
waking kwalletd, which prompts for an unlock on every launch because
kwallet-pam is not installed. Confirmed with dbus-monitor: no
org.kde.KWallet traffic on launch. The trade-off — tokens obfuscated
on disk rather than sealed behind a wallet password — and the route
back to the wallet are documented in the module and the README.
No-ops on headless hosts; DOTFILES_VSCODE overrides either way.
Verified per verify-module: shellcheck -s sh clean, idempotent re-run
on both paths, and fresh install plus re-run as root on oraclelinux:9
and debian:13.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
133 lines
4.4 KiB
Bash
133 lines
4.4 KiB
Bash
#!/usr/bin/env sh
|
|
# lib/fetch.sh — helpers for installing userland binaries into ~/.local/bin.
|
|
#
|
|
# Everything lands under $HOME, so installed tools survive Kasm redeploys
|
|
# (which wipe the root filesystem but keep $HOME) with no reinstall needed.
|
|
# No sudo, no system packages. Prefer statically linked release binaries so
|
|
# they keep working even after the root fs — and its shared libraries — is
|
|
# reset.
|
|
#
|
|
# Source this (callers already run `set -eu`), then use fetch_arch /
|
|
# fetch_arch_deb, have_version + mark_version, install_tarball_bin,
|
|
# install_tarball_tree, and install_raw_bin.
|
|
|
|
BIN_DIR="${BIN_DIR:-$HOME/.local/bin}"
|
|
VERSION_DIR="$BIN_DIR/.versions"
|
|
|
|
fsay() { printf '%s\n' "$*" >&2; }
|
|
|
|
# Normalize `uname -m` to the tokens rust-style release names use.
|
|
fetch_arch() {
|
|
case "$(uname -m)" in
|
|
x86_64|amd64) printf 'x86_64\n' ;;
|
|
aarch64|arm64) printf 'aarch64\n' ;;
|
|
*) fsay "fetch: unsupported arch $(uname -m)"; return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# Same, but in the Go/Debian token style most devops tools use.
|
|
fetch_arch_deb() {
|
|
case "$(uname -m)" in
|
|
x86_64|amd64) printf 'amd64\n' ;;
|
|
aarch64|arm64) printf 'arm64\n' ;;
|
|
*) fsay "fetch: unsupported arch $(uname -m)"; return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# Download <url> to <dest-file> using whatever downloader is available.
|
|
_fetch_dl() {
|
|
if command -v curl >/dev/null 2>&1; then
|
|
curl -fsSL "$1" -o "$2"
|
|
elif command -v wget >/dev/null 2>&1; then
|
|
wget -qO "$2" "$1"
|
|
else
|
|
fsay "fetch: need curl or wget"; return 1
|
|
fi
|
|
}
|
|
|
|
# have_version <name> <version> — true if <name> is installed and was
|
|
# stamped at <version>, so bootstrap can skip a re-download. Versions are
|
|
# recorded in ~/.local/bin/.versions rather than parsed from each tool's
|
|
# `--version` output, which varies wildly (kubectl/helm/k9s have no uniform
|
|
# flag).
|
|
have_version() {
|
|
[ -x "$BIN_DIR/$1" ] && [ -f "$VERSION_DIR/$1" ] &&
|
|
[ "$(cat "$VERSION_DIR/$1")" = "$2" ]
|
|
}
|
|
|
|
# mark_version <name> <version> — record the installed version.
|
|
mark_version() {
|
|
mkdir -p "$VERSION_DIR"
|
|
printf '%s\n' "$2" >"$VERSION_DIR/$1"
|
|
}
|
|
|
|
# install_tarball_bin <url> <path-within-archive> <dest-name> — download a
|
|
# .tar.gz, extract, and install one binary from it into ~/.local/bin.
|
|
install_tarball_bin() {
|
|
mkdir -p "$BIN_DIR"
|
|
_tmp="$(mktemp -d)"
|
|
_fetch_dl "$1" "$_tmp/archive.tar.gz"
|
|
tar -xzf "$_tmp/archive.tar.gz" -C "$_tmp"
|
|
install -m 0755 "$_tmp/$2" "$BIN_DIR/$3"
|
|
rm -rf "$_tmp"
|
|
fsay "installed $BIN_DIR/$3"
|
|
}
|
|
|
|
# install_zip_bin <url> <path-within-archive> <dest-name> — like
|
|
# install_tarball_bin but for .zip assets (e.g. 1Password CLI). Uses unzip
|
|
# when present, else python3's zipfile module, so no hard dependency on
|
|
# either being installed.
|
|
install_zip_bin() {
|
|
mkdir -p "$BIN_DIR"
|
|
_tmp="$(mktemp -d)"
|
|
_fetch_dl "$1" "$_tmp/archive.zip"
|
|
if command -v unzip >/dev/null 2>&1; then
|
|
unzip -q "$_tmp/archive.zip" -d "$_tmp"
|
|
elif command -v python3 >/dev/null 2>&1; then
|
|
python3 -m zipfile -e "$_tmp/archive.zip" "$_tmp"
|
|
else
|
|
fsay "fetch: need unzip or python3 to extract zip"; rm -rf "$_tmp"; return 1
|
|
fi
|
|
install -m 0755 "$_tmp/$2" "$BIN_DIR/$3"
|
|
rm -rf "$_tmp"
|
|
fsay "installed $BIN_DIR/$3"
|
|
}
|
|
|
|
# install_tarball_tree <url> <root-within-archive> <dest-dir> — download a
|
|
# .tar.gz that holds a whole application directory rather than one binary, and
|
|
# swap it into <dest-dir>. install_tarball_bin cannot be reused here: VS Code
|
|
# is an Electron bundle of thousands of files that only runs from an intact
|
|
# tree, not a single binary that can be picked out of the archive.
|
|
#
|
|
# The new tree is staged beside the target and moved into place only once the
|
|
# download and extraction have both succeeded, so an interrupted run leaves the
|
|
# previously installed version intact instead of a half-extracted directory.
|
|
install_tarball_tree() {
|
|
_dest="$3"
|
|
_tmp="$(mktemp -d)"
|
|
_fetch_dl "$1" "$_tmp/archive.tar.gz"
|
|
tar -xzf "$_tmp/archive.tar.gz" -C "$_tmp"
|
|
if [ ! -d "$_tmp/$2" ]; then
|
|
fsay "fetch: '$2' not found in archive"; rm -rf "$_tmp"; return 1
|
|
fi
|
|
mkdir -p "$(dirname "$_dest")"
|
|
rm -rf "$_dest.new" "$_dest.old"
|
|
mv "$_tmp/$2" "$_dest.new"
|
|
if [ -e "$_dest" ]; then
|
|
mv "$_dest" "$_dest.old"
|
|
fi
|
|
mv "$_dest.new" "$_dest"
|
|
rm -rf "$_dest.old" "$_tmp"
|
|
fsay "installed $_dest"
|
|
}
|
|
|
|
# install_raw_bin <url> <dest-name> — download a single raw binary.
|
|
install_raw_bin() {
|
|
mkdir -p "$BIN_DIR"
|
|
_tmp="$(mktemp)"
|
|
_fetch_dl "$1" "$_tmp"
|
|
install -m 0755 "$_tmp" "$BIN_DIR/$2"
|
|
rm -f "$_tmp"
|
|
fsay "installed $BIN_DIR/$2"
|
|
}
|